ページ

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 をつかってコードから生成しました。

1 件のコメント:

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