Erlang製のWebフレームワークMochiWebをさらっと触ってみたでござるの巻(その5)

さて、なんかMochiWebをさらっと触ってみたシリーズも第5弾となりました。ちょっとこのタイトルではじめた事を後悔しつつありますが、気を取り直していってみましょ。

過去記事はこちら
Erlang製のWebフレームワークMochiWebをさらっと触ってみたでござるの巻
Erlang製のWebフレームワークMochiWebをさらっと触ってみたでござるの巻(その2)
Erlang製のWebフレームワークMochiWebをさらっと触ってみたでござるの巻(その3)
Erlang製のWebフレームワークMochiWebをさらっと触ってみたでござるの巻(その4)

今回はSNSなどでは必須となるログイン機構と、それに伴うセッションの実装です。
僕自身、ログインに関しては今までほとんどをフレームワーク任せだったのでちと緊張しますが頑張ってみます←
ご指摘等は歓迎しますので泣かない程度にやってやってください。

セッションの基本的な動作
セッションはサーバ側に各クライアント毎のデータを保存しておいてページ感で情報を共有する為の仕組みです。
そして通常は、どのデータがどのクライアントのものかを見分ける為にクッキーを使います。クッキーに保存したキーを元にして、そのクライアントのデータを探し出すわけです。
従って一般的な流れとしては

  1. ログイン
  2. セッションにログイン情報を保存
  3. クライアントのクッキーにに、セッションIDを保存
  4. 次回からのアクセスでは都度、クライアントからセッションIDが送られて来るので、それをもとにセッションデータを参照、更新する

という流れとなります。

実装
上記を実装したコードが以下になります。
今回は動作原理を理解する為にmemcachedのようなキーバリューストアは使いません。てか、データベースすら使わずハードコーディングしています(笑) まあ何事も一歩一歩という事でw

-module(session_action).

-include_lib("eunit/include/eunit.hrl").

-export([start/1, stop/0]).


start(Port) ->
    mochiweb_http:start([{port, Port}, {loop, fun dispatch/1}]).

stop()->
    mochiweb_http:stop().

%%%===================================================================
%%% Reqest Dispatcher
%%%===================================================================

dispatch(Req) ->
    Path = Req:get(path),
    Params = Req:parse_qs(),

    case string:tokens(Path, "/") of
        [Controller, Action | UrlParams] ->
            handle(Controller, Action, UrlParams, Params, Req);
        [Controller] ->
            handle(Controller, "index", [], Params, Req);
        [] ->
            handle("top", "index", [], [], Req)
    end.

%%%===================================================================
%%% Reqest Handlers
%%%===================================================================

%% Topページ
handle("top", "index", _Path, _Params, Req) ->
    Req:ok({"text/html", "Welcome!"});

%% ユーザページ
handle("user", "welcome", [Name], _Params, Req) ->
    Req:ok({"text/html", subst("Welcom(in url) ~s!", [Name])});

%% ユーザ名とパスワードで認証する。成功したらセッションIDをクッキーに保存する。
handle("auth", "login", [], Params, Req) ->
    Username = proplists:get_value("username", Params),
    Password = proplists:get_value("password", Params),

    case authenticate(Username, Password) of
        {ok, UserId} ->
            SessionId  = get_session_keys(Username, Password, UserId),
            create_new_session(SessionId, UserId),
            Header1 = set_cookie("session_id", SessionId),
            Header2 = set_cookie("user_id", UserId),
            Req:ok({"text/html", [Header1, Header2], <<"ok! logged in :-)">>});
        {error, login_failure} ->
            Req:respond({403, [{"Content-Type", "text/html"}], 
                         <<"UnAuthenticated :-<">>})
    end;

%% ログアウト処理
handle("auth", "logout", [], _Params, Req) ->
    Header1 = set_cookie("session_id", ""),
    Header2 = set_cookie("user_id", ""),
    Req:ok({"text/html", [Header1, Header2], <<"logged out.">>});

%% ログイン後でないと見る事の出来ないコンテンツ
handle("secure_space", "index", [], _Params, Req) ->
    case check_loggedin(Req) of
        ok ->
            Name = get_session_value("name", Req),
            Req:ok({"text/html", subst("There is Secure Space (~s)", [Name])});
        {error, unauthenticated} ->
            Req:respond({403, [{"Content-Type", "text/html"}], 
                         <<"UnAuthenticated :-<">>})
    end;

%% 404 Not Found
handle(_, _, _, _, Req) ->
    Req:not_found().

%%%===================================================================
%%% Internal Functions
%%%===================================================================

set_cookie(Key, Value) ->
    mochiweb_cookies:cookie(Key, Value, [{path, "/"}]).


%% 既にログイン済みかどうか確認する。
check_loggedin(Req) ->
    case get_session_value("loggedin", Req) of
        true -> ok;
        _ -> {error, unauthenticated}
    end.

%% ユーザ認証を行う。
authenticate(Username, Password) ->
    case [Username, Password] of
        ["shin", "mypassword1"] -> {ok, "1"};
        ["tuna", "mypassword2"] -> {ok, "2"};
        _                       -> {error, login_failure}
    end.

%% ユーザ情報からセッションIDを生成する。
get_session_keys(Username, _Password, _UserId) ->
    case Username of
        "shin" -> "aaabbbcccddd";
        "tuna" -> "eeefffggghhh"
    end.     

%% セッションデータから与えられたキーに対応する値を返す。
get_session_value(Key, Req) ->
    case Req:get_cookie_value("session_id") of
        undefined ->  undefined;
        SessionId ->
            SessionData = get_session_data(SessionId),
            UserId = Req:get_cookie_value("user_id"),
            case proplists:get_value("id", SessionData) of
                UserId -> proplists:get_value(Key, SessionData);
                _ -> undefined
            end
    end.

%% ログイン時にセッションにユーザデータ等を保存する関数。今回はなにもしない。
create_new_session(_SessionId, _UserId) ->
    ok.

%% 与えられたセッションキーに対応するセッション情報を返す。
get_session_data(SessionId) ->
    case SessionId of
        "aaabbbcccddd" ->
            [{"id", "1"},
             {"name", "shin"}, 
             {"age", 36}, 
             {"sex", "male"},
             {"lang", ["ruby", "objective-c", "erlang"]},
             {"loggedin", true}];
        "eeefffggghhh" ->
            [{"id", "2"},
             {"name", "tuna"}, 
             {"age", 3}, 
             {"sex", "male"},
             {"lang", ["cat-lang"]},
             {"loggedin", true}];
        _ ->
            []
    end.

subst(Template, Values) ->
    list_to_binary(lists:flatten(io_lib:fwrite(Template, Values))).

いろいろとつっこみ所があるのは間違いないです(笑)
まあ、何はともあれ遊んでみましょうw

動作検証
今回はchromeを使いました。chromeではDeveloper Toolsにクッキーの値が見れるツールがあるので便利です。

まずはいつも通りErlangノードを起動してコンパイルします

$ erl -pa ../mochiweb
c(session_action).

9999番ポートでWebサーバを起動します。

session_action:start(9999)

ブラウザで http://localhost:9999 にアクセスすると。

ちなみにこの時点でのクッキーは

なんもありません。

では、この状態で認証が必要な http://localhost:9999/secure_space へアクセスしてみます。

見事に蹴られましたw

では
http://localhost:9999/auth/login?username=shin&password=mypassword1
にアクセスしてログインしてみます。ちなみに、ここでは思いっきりgetパラメータとしてユーザIDとパスワードを渡してますが、これは本番ではやっちゃいけないです。必ずpostとして渡すべきです。
ここではpostログインの為のページ作るのめんどくさいのでgetで渡していますw
結果は

おkみたいです。あっさりしてて実感ができませんがログインできました。

再び、さきほど表示出来なかったページを開いてみます。
http://localhost:9999/secure_space

おお〜。表示されました。

ちなみにこの時のクッキーの状態は

お、ちゃんとセッションIDとユーザIDが保存されています。

所感
今回はまずはコード内で全てすましてセッションとそれを使ったログイン機構を実装してみました。
殆どの場合、こういったものはフレームワークプラグインとして実装されているのであまり自分で実装する機会は少ないですが実際やってみると割と簡単ですね。
次回は何らかのキーバリューストアを使って実際にセッションデータを保存してみたいと思います。ErlangについてくるMnesiaを使うか、少し前にこのブログでも取り上げたredisを使うか悩んでるのですがどうしよう...

ちなみに僕は実際の開発でも、こんな感じでとりあえずハードコーディングしておいて後から関数/メソッドの中を本来の形に実装する事があります。いっぺんに全てをちゃんと実装するより僕の場合はこっちのがやりやすいです。