ページ

2010-12-25

HTTP と WebSocket でセッションを共有する

こんにちは、 nodejs.jp@masahirohです。 JavaScript Advent Calendar の最終日です。クリスマスだけど空気を読まずに Node.js の地味な話をします。ごめんなさい。

さて、 Node.js では Socket.IO を使えば、 WebSocket を使ったアプリケーションを割と簡単に作れるわけですが、これを Web フレームワークと一緒に使う場合、どうやってセッションを共有したらいいんだろう?とふと思いました。ユーザ名とパスワードをメッセージにのせたらいいんですかね? いや、セッション ID をメッセージにのせればいいのかな? うーん・・と思いながら Github をうろうろしていたら SessionWebSocket というアプリケーションを見つけました。セッション管理機能を追加する方法が結構参考になったので、コードリーディングをします、の予定だったのですが、動きがおかしかったので、一部書き直しつつ、コードリーディングします。説明の都合上、元のコメントは残していません。


解説に使ったソースコードは ここ の ma-read ブランチにあります。


では、解説していきます。まず、おおまかに言うと、セッション管理の仕組みは次のようになっています。


  1. ユーザが最初にページを開いたときに、クライアントは XHR でセッションのトークンを要求します。
  2. サーバはそのトークンをキーとしてセッションオブジェクトを管理しています。クライアントからの要求に応えて、トークンを返します。
  3. トークンを受け取ったら、 WebSocket のコネクションを開いて、そのトークンを送信します。
  4. WebSocket サーバはトークンを受け取ったら、管理しているセッションの有効期限をチェックし、有効ならセキュアなセッションとみなします。

server.js

では、サーバ側のコードから見ていきます。

依存しているモジュールをインポートしています。インポートしているのは、 Connect, Socket.IO, SessionWebSocket です。
var connect = require("connect");
var io = require("socket.io");
var sws = require("../sws.js")();
まずは Connect で Web サーバをつくります。 Connect は Web アプリケーション用のミドルウェアフレームワークです。決められたインターフェースに従ってつくられたアプリケーションを pluggable なミドルウェアとして扱うことができます。

createServer の引数に渡しているのがミドルウェアです。
var server = connect.createServer(
HTTP のセッションを管理しています。
  connect.cookieDecoder(),
  connect.session(),
今回の主役のひとり。セッションのトークンを発行します。
  sws.http,
これは静的ファイルを扱います。
  connect.staticProvider(__dirname+"/static")
);
サーバをポート 8000 番で起動します。
server.listen(8000);
次に、 WebSocket のサーバを準備します。

さきほどの Web サーバが WebSocket を扱えるようにしてやります。
var socket = io.listen(server);
イベントハンドラを設定します。引数に渡している sws.ws がもうひとりの主役です。 WebSocket 側のセッションを管理しています。こんなふうにコールバックにコールバックを渡すあたりがたまらないですね。
socket.on('connection', sws.ws(function(client) {
認証済みの場合のイベントハンドラです。セッション ID を出力してみます。
  client.on("secure", function() {
    console.log("SECURE");
    console.log(client.session.req.sessionID);
  });
認証が済んでいない場合のイベントハンドラです。
  client.on("insecure", function() {
    console.log("INSECURE ACCESS");
  });
メッセージ受信時のイベントハンドラです。
  client.on("message", function(msg) {
    client.send(msg);
    console.log("MSG:"+msg);
  });
}));

sws.js

var util = require('util');
では、いよいよセッション管理のミドルウェアについて見ていきます。

関数を exports にセットしています。先ほどアプリケーション側で require したときに呼んでいました。引数からオプションを受け取るために関数にしていたわけですね。
module.exports = function verifier(options)
{
デフォルトの設定です。セッションの生存期間 (ttl = time to live) を設定しています。
  var defaults = {
    ttl: 30*1000 // 30 秒
  };
引数でもらったオプションを反映させています。
  for (var k in options) {
    defaults[k] = options[k];
  }
複数のセッションを扱うためのオブジェクトです。
  var session_jar = {};
この関数は、セッションのトークンを発行するミドルウェアと、 WebSocket 用のセッション管理の関数をプロパティにもつオブジェクトを返します。
  return {
先ほど createServer に渡していたミドルウェアです。 リクエストオブジェクト、レスポンスオブジェクト、next という関数を引数にとる、というのが決められたインターフェースです。
    http:function give_token(req, res, next) {
リクエストのヘッダを参照し、 'x-access-request-token' フィールドが 'simple' だったらトークンを生成します。
      if (req.headers["x-access-request-token"]) {
        if (req.headers["x-access-request-token"].toLowerCase()==="simple") {
          var token = Math.random();
ユニークな値をとれるまで繰り返します。
          while (session_jar[token]) {
            token = Math.random();
          }
トークンをキーとしてセッションデータと発行した日時を保存しています。 req.session は connect.session が生成したものです。
          var tmp = Date.now();
          session_jar[token] = {
            session: req.session,
            date: tmp,
            id: req.sessionID
          };
レスポンスを生成します。トークンと発行日時を JSON で返しています。
          res.writeHead(200);
          res.end('{"x-access-token": "'+token+';'+tmp+'"}');
          return;
        }
      }
'x-access-request-token' フィールドがリクエストのヘッダにない場合は、このミドルウェアはやることがないので、 next を呼んで次のミドルウェアに処理をまかせます。
      if (next) {
        next();
      }
    }
こちらは WebSocket のセッションを管理する関数です。
    , ws: function attach_client(cb) {

      return function route_client(client) {
トークンの有効性をチェックする関数を定義します。
        function verify(token) {
          var tmp = session_jar[token];
セッションの期限切れをチェックしています。認証されたら削除しているので、トークンは1回だけしか使わない仕様になっています。
          if (tmp && tmp.date > Date.now() - defaults.ttl) {
            var session = tmp;
            delete session_jar[token];
            return session;
          }
          return false;
        }
'message' に対するイベントハンドラを設定します。トークンをチェックしています。この関数を実行するのは最初にメッセージを受信したときだけです。
        client.once('message', function first_verify(msg) {
メッセージで渡されたトークンが有効な場合は、クライアントにセッションデータを設定して、'secure' イベントをクライアントに対して発行します。
          var session = verify(msg) || false;
          if (session) {
            client._session = session;
            client.session = session.session;
            client.emit("secure");
セッションが有効だったので、 client.on を元に戻して、退避しておいた 'message' に対するイベントハンドラをバインドします。退避する処理は下の方にでてきます。
            client.on = oldon;
            for (var i = 0, l = onmsgs.length; i < l; i++) {
              client.on('message', onmsgs[i]);
            }
          }
セッションデータがないので 'insecure' イベントをクライアントに対して発行します。
          else {
            client.emit("insecure");
          }
        });
'message' イベントに対するイベントハンドラは、セッションが有効かどうかのチェックが済んだあとから実行されるようにしたいので、 client.on を 'message' イベントのハンドラだけ退避するように書き換えます。
        var onmsgs = [];
        var oldon = client.on;
        client.on = function(name, fn) {
          if (name === "message") onmsgs[onmsgs.length] = fn;
          else oldon.apply(this, arguments);
        };
        
呼び出し元にクライアントを渡します。
        cb(client);
      };
    }
  };
};

client.js

次にクライアント側を見ていきます。

SessionWebSocket で、トークンのやりとりをしてから、 'message' に対するイベントハンドラをバインドします。
SessionWebSocket(function(socket){
 socket.on('message',function(msg){
  console.log("SWS:",msg);
  });
セッションが確立した場合、このメッセージは受信されます。
  setInterval(function() {
    socket.send('Succeed!');
  }, 1000);
});
セキュアでないコネクションを示すための例です。こちらで送信したメッセージは弾かれます。
var socket = new io.Socket()
socket.connect();
socket.send("OH NOES");

sws.js

次に クライアントサイドのモジュールのコードです。トークンを取得して、返ってきたらコネクションを張り直します。
function SessionWebSocket(cb) {
  var xhr = new XMLHttpRequest()
  xhr.open("GET","/?no-cache="+(new Date()+0));
トークンを取得するためのヘッダを設定します。
  xhr.setRequestHeader("x-access-request-token","simple");
レスポンスに対するコールバックを設定します。
  xhr.onreadystatechange = function xhrverify() {
受信完了
    if (xhr.readyState === 4) {
      var tmp;
      try {
トークンが返ってきていたら、新規に WebSocket の接続を開始します。
        if (tmp = JSON.parse(xhr.responseText)["x-access-token"]) {
          var socket = new io.Socket();
          cb(socket);
          socket.connect();
取得したトークンを送っています
          socket.send(tmp.split(";")[0]);
        }
      }
      catch(e) {
        throw new Error("XMLHttpResponse had non-json response, possible cache issue?")
      }
    }
  };
リクエストを送信します。
  xhr.send();
}




以上でおしまいです。個人的には Connect のミドルウェアとしてセッション管理を実装するあたりや、イベントハンドラを一旦避けるあたりの処理が面白かったです。改善の余地がちょこちょこあるので、手を入れていこうと思います。


* このドキュメントは docco をつかってコードから生成しました。

11 件のコメント:

  1. Thanks for sharing, nice post! Post really provice useful information!

    Giaonhan247 chuyên dịch vụ ship hàng từ Mỹ về Việt Nam từ dịch vụ mua hàng mỹ online cùng với hướng dẫn cách tính thuế khi mua hàng trên amazon khi mua đồng hồ sale off trên amazon uy tín.

    返信削除
  2. flipkart sbi,
    flipkart sbi debit card emi,
    flipkart sbi,
    flipkart sbi offer,
    flipkart offer with sbi card,
    flipkart sbi credit card offers,
    flipkart sbi debit card offer,
    flipkart sbi debit card emi eligibility sms,
    sbi flipkart eligibility,
    flipkart sbi debit card emi eligibility check,
    flipkart sbi offer big billion day,
    flipkart sbi card,
    flipkart sbi emi debit card,
    flipkart debit card emi sbi sms,
    sbi woohoo flipkart gift card,
    flipkart sbi offer 2017,
    flipkart sbi offer 2018,
    flipkart sbi offer terms and conditions,
    flipkart debit card emi sbi mobile,
    flipkart offer for sbi debit card,
    flipkart emi with sbi debit card,
    flipkart coupons for sbi credit card,
    flipkart sbi debit card emi 2018,,
    flipkart emi through sbi debit card,
    flipkart debit card emi sbi in hindi,
    flipkart sbi debit card emi eligibility check sms,
    flipkart sbi emi offer,
    flipkart sbi terms and conditions,
    flipkart sbi offer september 2017,
    flipkart diwali sale sbi offer,
    flipkart sbi debit card,
    sbi flipkart link,
    flipkart sbi offer t&c,
    flipkart and sbi,
    flipkart and sbi card offer,
    flipkart and sbi debit card offer,
    flipkart mobile sbi card offer,
    flipkart sbi 10 cashback offer,
    flipkart sbi 10 offer,
    flipkart sbi card discount,
    flipkart sbi card offer big billion day,
    flipkart sbi card offer today,
    flipkart sbi discount,
    flipkart sbi instant discount,
    flipkart sbi offer big billion sale,
    flipkart sbi offer december 2017,
    flipkart sbi offer details,
    flipkart sbi offer july 2018,
    flipkart sbi offer today,
    sbi flipkart big billion offer,
    sbi po flipkart

    返信削除
  3. Nice work and I read your post, My friend recommended this blog and he was totally right keep up the good work, really, it left me so much impressed with this article, I hope you will have more great articles to share with readers, thank you the post.Thank you for this great article i read your post you really work well i learn a lot form your post keep it up. i follow your blog for learning something new.
    status about friendship
    खतरनाक शायरी 2019
    mahakal status in hindi
    cute love status in hindi

    返信削除
  4. "This is really interesting, you are such a great blogger. Visit Royal Digitech for creative and professional website design and Digital Marketing in Sirsa
    and Also get Digital Marketing Course in Sirsa
    "

    返信削除
  5. "Thanks for provide great informatic and looking beautiful blog, really nice required information & the things i never imagined and i would request, wright more blog and blog post like that for us. Thanks you once agian

    Free Classified in India"

    返信削除
  6. Your website has a lot of good articles about science, technology, life,... I also make websites, but I aim to entertain with light and interesting games. Come to my website by clicking the link below to explore it.

    Friday Night Funkin download

    Friday Night Funkin Online download

    返信削除

  • HOME
  • ABOUT
このページの先頭へ