play framework2.1で認証情報を保持する

ステートレスなフレームワーク

play frameworkはこれまでのServletコンテナなどと違いステートレスを特徴の一つとしたフレームワークです。ステートレスということは、基本的にセッションという概念が存在しない、いわゆるHTTP通信の本来あるべき姿に近い仕組みになります。

とはいうものの、ウェブシステムがすべてステートレスな仕組みで成り立つかというとそうはいきません。たとえば認証情報などはステートレスでは毎回認証を行う必要が出てくるためそもそもユーザビリティとの兼ね合いからも筋が悪い仕組みになります。(以前Willcomを使用していた頃、料金確認などをウェブサイトから閲覧できたのですが、毎回認証を行う必要があり非常に不便を感じました。)そこで、play frameworkでの状態管理について以下の通り考察してみました。

Cacheとsession

ステートレスなplay frameworkでどのように認証情報のようなサーバー・クライアント間の情報の保持を行えばいいのかが問題になりますが、主にcache と session を組み合わせることになります。 (cachesession はこちらを参照してください。基本的にplay framework 1.x系、2.x系で考え方は同じです。)

cache と session の特徴は以下の通りです。

  1. session:ServletコンテナやASP.NETなどで使用されるSessionとは異なり、クライアントのCookeiに情報を保存する仕組み。
  2. cache:サーバーサイドで一定期間情報を保持する仕組み。

railsを利用したことがある場合、sessionは初期状態のcookeiに保存する設定に該当すると理解すればOKです。)

これら二つの仕組みを使用して状態管理を行う方法ですが様々な方法が考えられますが、個人的には以下の方法がよいと考えています。

  1. ランダムな文字列を生成する。
  2. sessionに適切なキー値とともに、生成したランダムな文字列を保存する。
  3. cacheにランダムな文字列をキー値として状態管理したい情報を保存する。

これは、従来までのいわゆるセッションの考え方と同じです。セッションに関するランダムな文字列がクライアントのcookieとして保存されており、そのcookeiから取得した文字列を元にDBのようなキャッシュサーバーからデータを保存するという仕組みです。

具体的例

具体的な例として認証情報を保持する場合を想定します。(認証情報に該当するデータが存在する場合のみ想定します。)

  1. ユーザーはログイン画面でユーザーIDとパスワード(以下、認証情報)を入力する。
  2. システムはコントローラーのアクションメソッドで認証情報に該当するユーザーがデータベースに存在するか確認する。
  3. システムは認証情報に該当するデータが存在する場合は認証情報を保持して次の画面を表示する。(認証情報に該当するデータが存在しない場合はエラー画面を表示する。)
  4. ログイン画面以降の画面では、常にログインが行われているかどうか(認証情報が存在するか)をチェックする。

このような場合、3以降では保存された認証情報を保持することが認証が行われた事を表していることになります。

これらの処理についてcookeiとsessionを使用する3の処理は以下のようになります。

[scala] val userId = if (params.contains("userId")) params("userId").head else "" val pwd = if (params.contains("pwd")) params("pwd").head else "" val msg = "ユーザーIDまたはパスワードが正しくありません。"

if (userId == "" || pwd == "") { Redirect(routes.LoginController.show()).flashing( "error" -> msg ) } else { // パスワード文字列からsha-512文字列を生成する val shaPasswd = CryptUtil.getSha512(pwd, 1000)

// 認証情報が存在するか確認する
val userInfo = UserInfos.findByUserIdPassword(userId, shaPasswd)
if (userInfo == None) {
  Redirect(routes.LoginController.show()).flashing(
    "error" -> msg
  )
} else {

  // ランダムな文字列を生成する
  val key = UUID.randomUUID().toString

  // キャッシュを保存する
  Cache.set(key, userInfo)

  // セッションにはキャッシュのキーのみ保存する
  Redirect(routes.LedgerListController.index()).withSession(session + ("userInfo" -> key))
}

} [/scala]

この処理でのポイントは、クライアント側へデータを保存する session には認証に関する情報が保存しないことです。あくまでランダムな文字列を保存します。

一方で、ランダムな文字列をキーとして認証情報はサーバーサイドである cache へ保存します。こうすることで、仮にcookei内の情報を閲覧、変更された場合でもcacheのキー値を生成することは難しくなり認証情報が安全になります。(特にcacheのタイムアウト時間を設定することで、有限な時間ないにランダムな文字列を生成する必要があるため現実的には安全ではないかと考えています。)

こんな面倒なことをしないで session に認証情報となるデータを暗号化して保存すればよいという話もあります。しかし、暗号化された情報は復号されることがあり得るわけで不安です。

また、ウェブの世界では基本的に受信するパケットはダーティーであるという原則から考えると、個人的には積極的に選択しづらいと考えています。

最後に

ここまで play framework での状態管理について見てきましたが、そもそもは play framework はステートレスであると言うことが前提です。したがって、従来までのセッション管理という状態管理はできるだけ行わないようにするべきだと思います。そうすることでスケールメリットなどこれまでにないメリットを生かすことができるはずです。(この辺は試していないのでわからない…)

play framework をScalaで!

Scalaコンパイル時間が長いし、IDEの対応状況もいまいちでJava8が出てくれば取って代わられるのではないかという不満・不安もありますが、やはり型安全なRubyのようで書きやすいです。Scalaが気になる方はこの書籍のセットがおすすめです。

定番はコップ本です。とても分厚く重い書籍ですが、内容はわかりやすいです。一通り読むのに時間がかかりますが、折に触れて読み返すことになる本です。

開発をする場合には是非手元に置いておきたい1冊です。とにかくいろいろな情報が手軽に読めます。Scalaに関連するいろいろな情報をコンパクトに集めてきてまとめた1冊です。