Playframework で CSRF エラーを制御する

この記事は Play framework Advent Calendar 2014 - Adventar の 11 日目です。

10 日目は xuwei さんの 2014年のplayframeworkと自分のpull request でした。

Playframework では CSRF 対策が提供されています。

https://www.playframework.com/documentation/ja/2.3.x/ScalaCsrf

ただ実際に CSRF トークンエラーを発生させてみると、以下のような英文でテキスト文字列が出るだけのもので、ユーザーにとって次のアクションが取りづらいものになってしまっています。

f:id:ussy00:20141211002505p:plain

実際に自分で使おうとしたときに困ってしまったので、なんとか PR を出して自分で制御ができるようにしてもらいましたので、今日はこの場を借りて使い方を説明したいと思います。

CSRF エラーを制御するためには CSRFFiltererrorHandler 引数に CSRF.ErrorHandler trait を実装したクラスを定義すれば行えるようになってます。

今回は Playframework のエラーハンドリングの仕組みを利用するため、自分で例外クラスも定義して throw するようにしておきます。

import play.filters.csrf._

class CSRFTokenException(message: String) extends Exception(message: String)

object CSRFErrorHandler extends CSRF.ErrorHandler {
  def handle(request: RequestHeader, message: String) = throw new CSRFTokenException(message)
}

Controller 側では CSRFFilter を利用する箇所で、先ほど自分で定義したクラスを渡してあげます。

package controllers

import play.api._
import play.api.mvc._
import play.filters.csrf._

object Application extends Controller {

  def index = CSRFFilter(errorHandler = CSRFErrorHandler) { Action { implicit request =>
    Ok(views.html.index())
  }}

  def post = CSRFFilter(errorHandler = CSRFErrorHandler) { Action { implicit request =>
    Ok("nice post.")
  }}

}

続いてエラーハンドリングをするために Global オブジェクトを定義して、投げられてくる例外を抜き出して CSRFTokenException だったときの処理を書きます。

https://www.playframework.com/documentation/ja/2.3.x/ScalaGlobal

import play.api._
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent.Future
import controllers._

object Global extends GlobalSettings {

  override def onError(request: RequestHeader, ex: Throwable) = {
    ex.getCause match {
      case ex: CSRFTokenException =>
        // signout
        Future.successful(Redirect(routes.Signin.index))
      case _ =>
        Future.successful(InternalServerError(views.html.error(ex)))
    }
  }
}

この例では Rails のようにログインページにリダイレクトさせてみました。

なお View はドキュメントにあるとおり CSRF トークンを埋め込む感じになります。

@()(implicit request: RequestHeader)

@import helper._

<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    @form(routes.Application.post) {
      @CSRF.formField
      <input type="submit" value="post">
    }
  </body>
</html>

いかがでしたでしょうか? ErrorHandler trait を実装して上げることで、柔軟に CSRF のエラーハンドリングが行えるようになりました。

今回は Scala による使い方を説明しましたが Java でもアノテーションでハンドリング用の class を指定できるようになっています。

Java の Playframework ユーザーの方も是非試してみてください。

AngularJS で二重ポストしないようにする

これまで Ajax で二重ポストをしないようにするには、 DOM を操作して disable にして押せなくしたり、できないものは HTTP リクエストする前に変数に状態を持たせてレスポンスがきたらその状態をクリアしたりするといったものがありましたが、リクエストを出す処理ごとに二重ポストを防止するコードを入れなければいけないことが多く面倒でした。

もう少しスマートなやり方ないかな、と調べていたところ丁度使用している AngularJS でよい感じに解決しているブログを見つけましたので紹介します。

http://blog.codebrag.com/post/57412530001/preventing-duplicated-requests-in-angularjs

AngularJS は HTTP リクエスト後に結果が返ってきてないものを $http.pendingRequests に登録しています。二重ポストさせたくないリクエストにユニーク ID を HTTP リクエストに AngularJS へのオプションとして渡し、 pendingRequests の中身と比較して重複したものがあればリクエストを破棄するという AngularJS の仕組みを利用した面白い仕組みです。

こちらのアイデアをインターセプターにしてモジュール化したものを書いてみました。 AngularJS 1.2.13 で動作を確認しています。 (Array#filter を使ってるので古い IE をサポートする場合は変えてください)

angular.module("rejectDuplicatedRequest", []).config(["$provide", "$httpProvider", function($provide, $httpProvider) {
  var interceptor = ["$q", "$injector", function ($q, $injector) {
    return {
      request: function(config) {
        var $http = $injector.get("$http");
        var uniqueRequestOptionName = "unique";
        var requestIdOptionName = "requestId";

        function checkForDuplicates(config) {
          return !!config[uniqueRequestOptionName];
        }

        function checkIfDuplicated(config) {
          var duplicated = $http.pendingRequests.filter(function(pendingConfig) {
            return pendingConfig[requestIdOptionName] && pendingConfig[requestIdOptionName] === config[requestIdOptionName];
          });
          return duplicated.length > 0;
        }

        function buildRejectedRequestPromise(config) {
          var dfd = $q.defer();
          // var response = {data: {}, headers: {}, status: 499, config: config};
          // dfd.reject(response);

          return dfd.promise;
        }

        if (checkForDuplicates(config) && checkIfDuplicated(config)) {
          return buildRejectedRequestPromise(config);
        }

        return config || $q.when(config);
      }
    };
  }];

  $httpProvider.interceptors.push(interceptor);
});

このモジュールを読み込んだ後に AngularJS の HTTP リクエストに以下のようなオプションを渡せば、同じリクエストは連続して行えなくなります。従来のやり方に比べ利用側はリクエストの状態管理をしなくてすみ、ロジックが二重ポストのために汚染されないところが良い点です。

$http.post(url, data, {
  unique: true,
  requestId: "uniqueId"
}).success(function(response) {
  // process
}).error(function(response, status) {
  // error handling
});

先に紹介したブログでは $http から直接呼んだ場合しか動かないと書いていますが、こちらはインターセプターを直接登録しているため get/post のショートカットメソッドから呼び出しても動くようになっています。

それから、この例では二重ポストが実行された場合に無視するようにしていますが、 error メソッドのコールバックで何かしらハンドリングさせたい場合はコメントアウトしている reject を実行することで可能です。このときにステータスコード 499 を返していますが、標準化されてるものでもないので別なものにしてよいかもしれません。ただ 400 を返すだけでは通常のリクエストエラーで 400 を返すような場合に判別がつかないので、何かしら工夫が必要になりそうです。

あと $injector.get("$http")でわざわざ $http を取り出しているのが、ちょっとイケてないですが $httpProvider と一緒に利用しようとすると循環参照になってしまいエラーとなってしまうため止む得ずそうしてます。

Uncaught Error: [$injector:cdep] Circular dependency found: $http <- $compile

AngularJS の機能を利用して二重ポストをちょっとスマートに回避する方法を紹介してみました。

Playframework 2.x でタイプセーフに国際化対応のメール送信

この記事は Play framework 2.x Scala Advent Calendar 2013 の 5 日目です。

http://www.adventar.org/calendars/114

メールテンプレート

これまで Java のプロジェクトでは Velocity/FreeMarker/ozacc-mail といったライブラリを私はメールテンプレートに使ってきましたが、これらのライブラリに依存せず Play のテンプレート機能を使ってメール送信をしてみます。

最近ではクライアントサイド側でレンダリングすることも増え、サーバーサイド側のテンプレートの需要は減りつつありますが、メールは事前に書かれている必要がありますので、簡易なメールでなければ何かしらのテンプレートは欲しいところです。

Play の view テンプレート機能はタイプセーフに記述できるため、恐れずオブジェクトのプロパティ名を変更できます。また最近のフレームワークらしくデフォルトで HTML エスケープを有効にしてくれるため入力値を安全に埋め込められるメリットがあります。

メール機能の有効化

/build.sbt

libraryDependencies ++= Seq(
  jdbc,
  cache,
  "com.typesafe" %% "play-plugins-mailer" % "2.2.0"
)

まず play-plugins (https://github.com/typesafehub/play-plugins/tree/master/mailer) にメールライブラリが公開されているので /build.sbt に依存ライブラリとして登録します。

/conf/play.plugins

1500:com.typesafe.plugin.CommonsMailerPlugin

このライブラリはプラグインとして動作するので /conf/play.plugins ファイルを作成し、 Play アプリケーションから呼び出せるように登録します。

/conf/application.conf

# Mailer
# ~~~~~
smtp.host=localhost
smtp.mock=true

メールを送信するための設定を /conf/application.conf に事前に入れておく必要があります。今回は開発用ということでコンソールにログ出力されるだけの Mock 機能を利用します。

プロダクション環境などで実際にメールを飛ばす場合は別途設定ファイルを設けて、実行時に設定を切り替えられるようにしておきましょう。

これでアプリケーションから呼び出す準備ができました。

メールテンプレート作成

今回は HTML 形式とテキスト形式両方を送信してみようと思うので、それぞれテンプレートを用意します。なお Play の view テンプレートは HTML/XML/TEXT 形式に対応しています。

http://www.playframework.com/documentation/2.2.x/ScalaTemplates

/app/views/mail/registeredAccount.scala.txt

@(account: Account)(implicit lang: Lang)
@lang match {
    case Lang("ja", _) => {
進捗どうですか? @account.name & @account.mailAddress
    }
    case _ => {
How is your progress? @account.name & @account.mailAddress
    }
}

/app/views/mail/registeredAccount.scala.html

@(account: Account)(implicit lang: Lang)
@lang match {
    case Lang("ja", _) => {  
        <html>
            <body>
                <h1>進捗どうですか? @account.name &amp; @account.mailAddress</h1>
            </body>
        </html>
    }
    case _ => {
        <html>
            <body>
                <h1>How is your progress? @account.name &amp; @account.mailAddress</h1>
            </body>
        </html>
    }
}

メール送信

先ほど記述した HTML メール、テキストメールを指定し、 body メソッドを介してメールの内容を取得することが出来ます。 trim はテキストメールで前後に無駄な改行が入ってしまい、ユーザーが表示した際に不要に改行されてしまうのを防止するために入れています。

import play.api.Play.current
import com.typesafe.plugin._

val mail = use[MailerPlugin].email
mail.setSubject("Registered account")
mail.setRecipient(s"${account.name} <${account.mailAddress}>")
mail.setFrom("Playframework <noreply@example.com>")
mail.send(
  views.txt.mail.registeredAccount(account).body.trim,
  views.html.mail.registeredAccount(account).body.trim
)

メールテンプレートファイルを移動、リネームしてしまってもコンパイルエラーになりますので、リソースがみつからないという実行時エラーを避けられるメリットがあります。

国際化

メッセージ API と多言語対応にも記述されていますが、 /application.conf に言語設定を最初に行っておく必要があります。

application.langs="en,ja"

Play は HTTP リクエストに含まれるロケール情報、またはレスポンスを返すときに withLang メソッドによって生成された cookie によって言語切替が行えるようになっています。

そのためテンプレートに Lang オブジェクトを暗黙的引数として入れておけば、コントローラーから渡される RequestHeader からユーザーの言語情報を自動で取得できるようになり、あとはその渡された言語ごとにメールテンプレートを切り替えれば国際化対応が行えます。

match でわざわざ分岐させず Play が提供している Messages オブジェクトを介して外部ファイルによる国際化対応もできるでしょう。

まとめ

Scala 2.10 からは String Interpolation 機能によってタイプセーフに文字列を生成できますが、 今回は Play の view テンプレートを使うことで得られるいくつかのメリットを紹介してみました。

サンプルは Github に公開しています。

https://github.com/tsuyoshizawa/play-mail-app

明日は kyu999 さんです。お楽しみに。