Skip to content

Latest commit

 

History

History
772 lines (551 loc) · 30.4 KB

README.ja.md

File metadata and controls

772 lines (551 loc) · 30.4 KB

Play2.x module for Authentication and Authorization Build Status

これは Play2.x のアプリケーションに認証/認可の機能を手軽に組み込むためのモジュールです。

Scaladoc

  • play2-auth scaladoc
  • play2-auth-social scaladoc
  • play2-auth-test scaladoc

対象

このモジュールは Play2.x の __Scala__版を対象としています。

Play2.4.2 で動作確認をしています。

動機

安全性

標準で提供されている Security トレイトでは、ユーザを識別する識別子を規定していません。

サンプルアプリケーションのように、E-mailアドレスやユーザIDなどを識別子として利用した場合、 万が一Cookieが流出した場合に、即座にSessionを無効にすることができません。

このモジュールでは、暗号論的に安全な乱数生成器を使用してセッション毎にuniqueなSessionIDを生成します。 万が一Cookieが流失した場合でも、再ログインによるSessionIDの無効化やタイムアウト処理を行うことができます。

柔軟性

標準で提供されている Security トレイトでは、認証後に Action を返します。

これでは認証/認可以外にも様々なAction合成を行いたい場合にネストが深くなって非常に記述性が低くなります。

このモジュールでは Stackable-Controllerの実装や、 ActionRefiner による実装など、柔軟に他の操作を組み合わせて使用する方法を提供しています。

以前のバージョン

Play2.3.x 向けの使用方法は 0.13.5 READMEをご参照ください。 Play2.2.x 向けの使用方法は 0.11.1 READMEをご参照ください。

Play2.1以前をお使いの方へ

Play2.2 から Result が非推奨になりました。その影響で play2.auth のインターフェイスも変更されています。

0.10.1以前からバージョンアップを行う方はご注意ください。

導入

Build.scala もしくは build.sbt にライブラリ依存性定義を追加します。

    "jp.t2v" %% "play2-auth"        % "0.14.2",
    "jp.t2v" %% "play2-auth-social" % "0.14.2", // ソーシャルログイン
    "jp.t2v" %% "play2-auth-test"   % "0.14.2" % "test",
    play.sbt.Play.autoImport.cache // デフォルトのIdContainerを使う場合のみ必要です

For example: Build.scala

  val appDependencies = Seq(
    "jp.t2v" %% "play2-auth"        % "0.14.2",
    "jp.t2v" %% "play2-auth-social" % "0.14.2",
    "jp.t2v" %% "play2-auth-test"   % "0.14.2" % "test",
    play.sbt.Play.autoImport.cache // デフォルトのIdContainerを使う場合のみ必要です
  )

このモジュールはシンプルな Scala ライブラリとして作成されています。 play.plugins ファイルは作成する必要ありません。

使い方

  1. app/controllers 以下に jp.t2v.lab.play2.auth.AuthConfig を実装した trait を作成します。

    // (例)
    trait AuthConfigImpl extends AuthConfig {
    
      /**
       * ユーザを識別するIDの型です。String や Int や Long などが使われるでしょう。
       */
      type Id = String
    
      /**
       * あなたのアプリケーションで認証するユーザを表す型です。
       * User型やAccount型など、アプリケーションに応じて設定してください。
       */
      type User = Account
    
      /**
       * 認可(権限チェック)を行う際に、アクション毎に設定するオブジェクトの型です。
       * このサンプルでは例として以下のような trait を使用しています。
       *
       * sealed trait Role
       * case object Administrator extends Role
       * case object NormalUser extends Role
       */
      type Authority = Role
    
      /**
       * CacheからユーザIDを取り出すための ClassTag です。
       * 基本的にはこの例と同じ記述をして下さい。
       */
      val idTag: ClassTag[Id] = classTag[Id]
    
      /**
       * セッションタイムアウトの時間(秒)です。
       */
      val sessionTimeoutInSeconds: Int = 3600
    
      /**
       * ユーザIDからUserブジェクトを取得するアルゴリズムを指定します。
       * 任意の処理を記述してください。
       */
      def resolveUser(id: Id)(implicit ctx: ExecutionContext): Future[Option[User]] = Account.findByIdAsync(id)
    
      /**
       * ログインが成功した際に遷移する先を指定します。
       */
      def loginSucceeded(request: RequestHeader)(implicit ctx: ExecutionContext): Future[Result] =
        Future.successful(Redirect(routes.Message.main))
    
      /**
       * ログアウトが成功した際に遷移する先を指定します。
       */
      def logoutSucceeded(request: RequestHeader)(implicit ctx: ExecutionContext): Future[Result] =
        Future.successful(Redirect(routes.Application.login))
    
      /**
       * 認証が失敗した場合に遷移する先を指定します。
       */
      def authenticationFailed(request: RequestHeader)(implicit ctx: ExecutionContext): Future[Result] =
        Future.successful(Redirect(routes.Application.login))
    
      /**
       * 認可(権限チェック)が失敗した場合に遷移する先を指定します。
       */
      override def authorizationFailed(request: RequestHeader, user: User, authority: Option[Authority])(implicit context: ExecutionContext): Future[Result] = {
        Future.successful(Forbidden("no permission"))
      }
    
      /**
       * 権限チェックのアルゴリズムを指定します。
       * 任意の処理を記述してください。
       */
      def authorize(user: User, authority: Authority)(implicit ctx: ExecutionContext): Future[Boolean] = Future.successful {
        (user.role, authority) match {
          case (Administrator, _) => true
          case (NormalUser, NormalUser) => true
          case _ => false
        }
      }
    
      /**
       * (Optional)
       * SessionID Tokenの保存場所の設定です。
       * デフォルトでは Cookie を使用します。
       */
      override lazy val tokenAccessor = new CookieTokenAccessor(
        /*
         * cookie の secureオプションを使うかどうかの設定です。
         * デフォルトでは利便性のために false になっていますが、
         * 実際のアプリケーションでは true にすることを強く推奨します。
         */
        cookieSecureOption = play.api.Play.isProd(play.api.Play.current),
        cookieMaxAge       = Some(sessionTimeoutInSeconds)
      )
    
    }
  2. 次にログイン、ログアウトを行う Controller を作成します。 この Controller に、先ほど作成した AuthConfigImpl トレイトと、 jp.t2v.lab.play2.auth.LoginLogout トレイトを mixin します。

    object Application extends Controller with LoginLogout with AuthConfigImpl {
    
      /** ログインFormはアプリケーションに応じて自由に作成してください。 */
      val loginForm = Form {
        mapping("email" -> email, "password" -> text)(Account.authenticate)(_.map(u => (u.email, "")))
          .verifying("Invalid email or password", result => result.isDefined)
      }
    
      /** ログインページはアプリケーションに応じて自由に作成してください。 */
      def login = Action { implicit request =>
        Ok(html.login(loginForm))
      }
    
      /**
       * ログアウト処理では任意の処理を行った後、
       * gotoLogoutSucceeded メソッドを呼び出した結果を返して下さい。
       *
       * gotoLogoutSucceeded メソッドは Future[Result] を返します。
       * 以下のようにflashingなどを追加することもできます。
       *
       *   gotoLogoutSucceeded.map(_.flashing(
       *     "success" -> "You've been logged out"
       *   ))
       */
      def logout = Action.async { implicit request =>
        // do something...
        gotoLogoutSucceeded
      }
    
      /**
       * ログイン処理では認証が成功した場合、
       * gotoLoginSucceeded メソッドを呼び出した結果を返して下さい。
       *
       * gotoLoginSucceeded メソッドも gotoLogoutSucceeded と同じく Future[Result] を返します。
       * 任意の処理を追加することも可能です。
       */
      def authenticate = Action.async { implicit request =>
        loginForm.bindFromRequest.fold(
          formWithErrors => Future.successful(BadRequest(html.login(formWithErrors))),
          user => gotoLoginSucceeded(user.get.id)
        )
      }
    
    }
  3. 最後は、好きな Controller に 先ほど作成した AuthConfigImpl トレイトと jp.t2v.lab.play2.auth.AuthElement トレイト を mixin すれば、認証/認可の仕組みを導入することができます。

    object Message extends Controller with AuthElement with AuthConfigImpl {
    
      // StackAction の 引数に権限チェック用の (AuthorityKey, Authority) 型のオブジェクトを指定します。
      // 第二引数に RequestWithAttribute[AnyContent] => Result な関数を渡します。
    
      // AuthElement は loggedIn[A](implicit RequestWithAttribute[A]): User というメソッドをもっています。
      // このメソッドから認証/認可済みのユーザを取得することができます。
    
      def main = StackAction(AuthorityKey -> NormalUser) { implicit request =>
        val user = loggedIn
        val title = "message main"
        Ok(html.message.main(title))
      }
    
      def list = StackAction(AuthorityKey -> NormalUser) { implicit request =>
        val user = loggedIn
        val title = "all messages"
        Ok(html.message.list(title))
      }
    
      def detail(id: Int) = StackAction(AuthorityKey -> NormalUser) { implicit request =>
        val user = loggedIn
        val title = "messages detail "
        Ok(html.message.detail(title + id))
      }
    
      // このActionだけ、Administrator でなければ実行できなくなります。
      def write = StackAction(AuthorityKey -> Administrator) { implicit request =>
        val user = loggedIn
        val title = "write message"
        Ok(html.message.write(title))
      }
    
    }

テスト

play2.auth では、version 0.8 からテスト用のサポートを提供しています。

FakeRequest を使って Controller のテストを行う際に、 ログイン状態のユーザを指定することができます。

package test

import org.specs2.mutable._

import play.api.test._
import play.api.test.Helpers._
import controllers.{AuthConfigImpl, Messages}
import jp.t2v.lab.play2.auth.test.Helpers._

class ApplicationSpec extends Specification {

  object config extends AuthConfigImpl

  "Messages" should {
    "return list when user is authorized" in new WithApplication {
      val result = Messages.list(FakeRequest().withLoggedIn(config)(1))
      contentType(result) must equalTo("text/html")
    }
  }

}
  1. まず jp.t2v.lab.play2.auth.test.Helpers._ を import します。

  2. 次にテスト対象に mixin されているものと同じ AuthConfigImpl のインスタンスを生成します。

     object config extends AuthConfigImpl
    
  3. FakeRequestwithLoggedIn メソッドを呼び出します。

    • 第一引数には、先ほど定義した AuthConfigImpl インスタンス
    • 第二引数には、このリクエストがログインしている事にする、対象のユーザIDを指定します。

以上で play2.auth を使用したコントローラのテストを行うことができます。

高度な使い方

リクエストパラメータに応じて権限判定を変更する

例えば SNS のようなアプリケーションでは、メッセージの編集といった機能があります。

しかしこのメッセージ編集は、自分の書いたメッセージは編集可能だけども、 他のユーザが書いたメッセージは編集禁止にしなくてはいけません。

そういった場合にも以下のように Authority を関数にすることで簡単に対応が可能です。

trait AuthConfigImpl extends AuthConfig {

  // 他の設定省略

  type Authority = User => Future[Boolean]

  def authorize(user: User, authority: Authority)(implicit ctx: ExecutionContext): Future[Boolean] = authority(user)

}
object Application extends Controller with AuthElement with AuthConfigImpl {

  private def sameAuthor(messageId: Int)(account: Account): Future[Boolean] =
    Message.getAutherAsync(messageId).map(_ == account)

  def edit(messageId: Int) = StackAction(AuthorityKey -> sameAuthor(messageId)) { request =>
    val target = Message.findById(messageId)
    Ok(html.message.edit(messageForm.fill(target)))
  }

}

ログイン後、認証直前にアクセスしていたページに遷移する

アプリケーションの任意のページにアクセスしてきた際に、 未ログイン状態であればログインページに遷移し、 ログインが成功した後に最初にアクセスしてきたページに戻したい、といった要求があります。

その場合も以下のようにするだけで簡単に実現できます。

trait AuthConfigImpl extends AuthConfig {

  // 他の設定省略

  def authenticationFailed(request: RequestHeader)(implicit ctx: ExecutionContext): Future[Result] =
    Future.successful(Redirect(routes.Application.login).withSession("access_uri" -> request.uri))

  def loginSucceeded(request: RequestHeader)(implicit ctx: ExecutionContext): Future[Result] = {
    val uri = request.session.get("access_uri").getOrElse(routes.Message.main.url)
    Future.successful(Redirect(uri).withSession(request.session - "access_uri"))
  }

}

ログイン状態と未ログイン状態で表示を変える

トップページなどにおいて、未ログイン状態でも画面を正常に表示し、 ログイン状態であればユーザ名などを表示する、といったことがしたい場合、 以下のように AuthElement の代わりに OptionalAuthElement を使用することで実現することができます。

OptionalAuthElement を使用する場合、Authority は必要ありません。

object Application extends Controller with OptionalAuthElement with AuthConfigImpl {

  // maybeUser is an Option[User] instance.
  def index = StackAction { implicit request =>
    val maybeUser: Option[User] = loggedIn
    val user: User = maybeUser.getOrElse(GuestUser)
    Ok(html.index(user))
  }

}

認証だけ行って認可は行わない。

認証だけ行うこともできます。

AuthElement の代わりに AuthenticationElement を使うだけです。 この場合、 AuthorityKey の指定は必要ありません。

object Application extends Controller with AuthenticationElement with AuthConfigImpl {

  def index = StackAction { implicit request =>
    val user: User = loggedIn
    Ok(html.index(user))
  }

}

Ajaxリクエスト時の認証失敗で401を返す

通常のアクセスで認証が失敗した場合にはログイン画面にリダイレクトさせたいけれども、 Ajaxリクエストの場合には単に401を返したい場合があります。

その場合でも以下の様に authenticationFailed で分岐すれば実現することができます。

def authenticationFailed(request: RequestHeader)(implicit ctx: ExecutionContext) = Future.successful {
  request.headers.get("X-Requested-With") match {
    case Some("XMLHttpRequest") => Unauthorized("Authentication failed")
    case _ => Redirect(routes.Application.login)
  }
}

他のAction操作と合成する

stackable-controller の仕組みを使用します。

例えば、CSRF対策で各Actionでトークンのチェックをしたい、としましょう。

全てのActionで毎回チェックロジックを書くのは大変なので、以下のようなトレイトを作成します。

import jp.t2v.lab.play2.stackc.{RequestWithAttributes, StackableController}
import scala.concurrent.Future
import play.api.mvc.{Result, Request, Controller}
import play.api.data._
import play.api.data.Forms._

trait TokenValidateElement extends StackableController {
    self: Controller =>

  // Token の発行処理は省略

  private val tokenForm = Form("token" -> text)

  private def validateToken(request: Request[_]): Boolean = (for {
    tokenInForm <- tokenForm.bindFromRequest()(request).value
    tokenInSession <- request.session.get("token")
  } yield tokenInForm == tokenInSession).getOrElse(false)

  override def proceed[A](request: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = {
    if (validateToken(request)) super.proceed(request)(f)
    else Future.successful(BadRequest)
  }

}

この TokenValidateElement トレイトと AuthElement トレイトを両方mixinすることで、 CSRFトークンチェックと認証/認可を両方行うことができます。

object Application extends Controller with TokenValidateElement with AuthElement with AuthConfigImpl {

  // Token の発行処理は省略

  def page1 = StackAction(AuthorityKey -> NormalUser) { implicit request =>
    // do something
    Ok(html.page1("result"))
  }

  def page2 = StackAction(AuthorityKey -> NormalUser) { implicit request =>
    // do something
    Ok(html.page2("result"))
  }

}

非同期サポート

効率的なアプリケーションを作成するため、昨今ではReactiveなアプローチが人気を博しています。 Playはこういった非同期なアプローチが得意であり、ReactiveMongoScalikeJDBC-Async などといった非同期なライブラリを上手に使用する事ができます。

StackAction の代わりに AsyncStack を使用することで、 Future[Result] を返すアクションを簡単につくることができます。

trait HogeController extends AuthElement with AuthConfigImpl {

  def hoge = AsyncStack { implicit req =>
    val messages: Future[Seq[Message]] = AsyncDB.withPool { implicit s => Message.findAll }
    messages.map(Ok(html.view.messages(_)))
  }

}

Stateless

このモジュールの標準実装はステートフルな実装になっています。 Play framefork が推奨するステートレスなポリシーを尊重したくはあるのですが、 ステートレスにすると次のようなセキュリティリスクが存在するため、標準では安全側に倒してあります。

例えば、インターネットカフェなどでサービスにログインし、 ログアウトするのを忘れて帰宅してしまった、といった場合。 ステートレスではその事実に気付いても即座にそのSessionを無効にすることができません。 標準実装ではログイン時に、それより以前のSessionを無効にしてます。 したがってこの様な事態に気付いた場合、即座に再ログインすることでSessionを無効化することができます。

このようなリスクを踏まえ、それでもステートレスにしたい場合、 以下のように設定することでステートレスにすることができます。

trait AuthConfigImpl extends AuthConfig {

  // 他の設定省略

  override lazy val idContainer: AsyncIdContainer[Id] = AsyncIdContainer(new CookieIdContainer[Id])

}

IdContainer は SessionID および UserID を紐付ける責務を負っています。 この実装を切り替えることで、例えば RDBMS に認証情報を登録するといった事も可能です。

なお、CookieIdContainer ではSessionタイムアウトは未サポートとなっています。

ActionFunction としての利用

Play2.2 から ActionBuilder が導入され、 Play2.3 から ActionBuilder をさらに抽象化した ActionFunction が導入されました。

ActionFunction の具象インターフェイスとして ActionBuilderActionRefiner があり、 更に ActionRefiner の具象インターフェイスとして ActionTransformerActionFilter が存在しています。

これらを組み合わせて様々な処理を合成した ActionBuilder を作成できるようになっています。

そのため、play2-auth でも様々な ActionFunction の実装を提供しています。 もし、他のライブラリや既存コードが ActionFunction を利用しているのであれば、 これらの使用も検討できます。

ActionBuilders

play2-auth が提供する ActionFunction 群を利用したい場合は、 AuthElement の代わりに AuthActionBuilders を Controller に mixin します。

StackActionAsyncStack の代わりに、OptionalAuthAction, AuthenticationAction および AuthorizationAction を利用することができます。

object Message extends Controller with AuthActionBuilders with AuthConfigImpl {

  import scala.concurrent.Future.{successful => future}

  /**
   * `OptionalAuthAction` の型は `ActionBuilder[OptionalAuthRequest]` です。
   * つまり、`OptionalAuthRequest => Result` という関数を受け取り `Action` を作成します。
   * 
   * `OptionalAuthRequest` は `user: Option[User]` というフィールドを持っています。
   * 認証が成功すれば `Some` を、失敗すれば `None` を保持しています。
   * 認可は行いません。
   */
  def index = OptionalAuthAction.async { request =>
    val maybeUser: Option[User] = request.user
    future(Ok(view.html.index(maybeUser.getOrElse(GuestUser))))
  }

  /**
   * `AuthenticationAction` の型は `ActionBuilder[AuthRequest]` です。
   * つまり、`AuthRequest => Result` という関数を受け取り `Action` を作成します。
   *
   * `AuthRequest` は `user: User` というフィールドを持っています。
   * 認証が成功していれば、受け取った `AuthRequest => Result` を実行し、
   * 失敗していれば、`AuthConfig` で定義された `authenticationFailed` を返す
   * `Action` を生成します。
   * 認可は行いません。
   */
  def notNeedAuthorization = AuthenticationAction.async { request =>
    val user: User = request.user
    future(Ok(view.html.messages(user)))
  }

  /**
   * `AuthorizationAction` は `Authority` を受け取って `ActionBuilder[AuthRequest]` を返す関数です。
   *
   * 認証が成功していれば、認可を行い、
   * 失敗していれば、`AuthConfig` で定義された `authenticationFailed` を返します。
   * 認可が成功していれば `AuthRequest => Result` を実行し、
   * 失敗していれば、`AuthConfig` で定義された `authorizationFailed` を返す `Action` を生成します。
   */
  def needAuthorization = AuthorizationAction(Admin).async { request =>
    val user: User = request.user
    future(Ok(view.html.messages(user)))
  }

}

ActionFunctions

上記の OptionalAuthAction, AuthenticationAction および AuthorizationActionActionBuilder なので、 このままでは他の ActionBuilder と合成することはできません。

他の ActionBuilder と合成が可能なように、 OptionalAuthFunction, AuthenticationRefiner および AuthorizationFilter が定義されています。

それぞれの型は以下のようになっています。

  val OptionalAuthFunction: ActionFunction[Request, OptionalAuthRequest]
  val AuthenticationRefiner: ActionRefiner[OptionalAuthRequest, AuthRequest]
  def AuthorizationFilter(authority: Authority): ActionFilter[AuthRequest]

したがって、他のライブラリで提供された、もしくは自分で定義した ActionBuilder[Request] が存在していれば、下記のように合成することが可能です。

object MyCoolAction extends ActionBuilder[Request] {
  ... 
}

object MyController extends Controller with AuthActionBuilders with AuthConfigImpl {

  val MyCoolOptionalAuthAction: ActionBuilder[OptionalAuthRequest] =
    MyCoolAction andThen OptionalAuthFunction

  val MyCoolAuthenticationAction: ActionBuilder[AuthRequest] =
    MyCoolOptionalAuthAction andThen AuthenticationRefiner

  def MyCoolAuthorizationAction(authority: Authority): ActionBuilder[AuthRequest] =
    MyCoolAuthenticationAction andThen AuthorizationFilter(authority)


  def index = MyCoolAuthorizationAction(Admin).async {
    ...
  }

}

独自リクエスト型を持つ ActionBuilder との合成

上記では ActionBuilder[Request] と合成する例を示しました。 しかし、実際には ActionBuilder が、独自のリクエスト型を扱っている場合があります。

例えばAction単位でトランザクションを表すようなものを考えた場合、 以下のような ActionBuilder を定義するかもしれません。

case class TxRequest[A](session: DBSession, underlying: Request[A]) extends WrappedRequest[A](underlying)

object TxAction extends ActionBuilder[TxRequest] {
  override def invokeBlock[A](request: Request[A], block: (TxRequest[A]) => Future[Result]): Future[Result] = {
    import scalikejdbc.TxBoundary.Future._
    implicit val ctx = executionContext
    DB.localTx { session =>
      block(new TxRequest(session, request))
    }
  }
}

こうした場合、OptionalAuthFunction はあくまで ActionFunction[Request, OptionalAuthRequest] のため TxAction と合成することができません。

また、仮に合成ができたとしても OptionalAuthRequestTxRequest の持つ session の事を知りようが無いので、 実際のAction処理中で DBSession を扱うことができません。

そこで play2-auth ではこれらの仕組みを更に抽象化した仕組みを提供しています。

下記のように GenericOptionalAuthRequestGenericAuthRequest また、 GenericOptionalAuthFunction, GenericAuthenticationRefiner および GenericAuthorizationFilter を使用すれば、 TxAction のような ActionBuilder とも合成が可能になります。

object MyController extends Controller with AuthActionBuilders with AuthConfigImpl {

  type OptionalAuthTxRequest[A] = GenericOptionalAuthRequest[A, TxRequest]
  type AuthTxRequest[A] = GenericAuthRequest[A, TxRequest]

  val OptionalAuthTxAction: ActionBuilder[OptionalAuthTxRequest] = 
    composeOptionalAuthAction(TxAction)

  val AuthenticationTxAction: ActionBuilder[AuthTxRequest] = 
    composeOptionalAuthAction(TxAction)

  def AuthorizationTxAction(authority: Authority): ActionBuilder[AuthTxRequest] = 
    composeAuthorizationAction(TxAction)(authority)

  /**
   * GenericOptionalAuthRequest および GenericAuthRequest は、
   * 第2型引数で指定されたリクエスト型を underlying というフィールドで提供します。
   * したがって、AuthTxRequest では、 TxRequest から DBSession を取得することが可能です。
   */
  def index = AuthorizationTxAction(Admin).async { request => 
    val user: User = request.user
    val session: DBSession = request.underlying.session
    ...
  }

}

この様にして、play2-auth では、任意の ActionBuilder と合成できる仕組みを提供しています。

しかし、独自リクエスト型を持つ ActionBuilder が複数存在し、その全てを合成しようとすると、 Play2 の現在の仕組みではできません。

したがって、基本的には [Stackable-Controller] の利用を推奨いたします。

サンプルアプリケーション

  1. git clone https://github.com/t2v/play2-auth.git
  2. cd play2-auth
  3. sbt "project sample" run
  4. ブラウザで http:https://localhost:9000/ にアクセス
    1. 「Database 'default' needs evolution!」と聞かれるので Apply this script now! を押して実行します

    2. 適当にログインします

      アカウントは以下の3アカウントが登録されています。

       Email             | Password | Role
       [email protected] | secret   | Administrator
       [email protected]   | secret   | NormalUser
       [email protected] | secret   | NormalUser
      

注意事項

このモジュールは Play2.x の Cache API を利用しています。

標準実装の Ehcache では、サーバを分散させた場合に正しく認証情報を扱えない場合があります。

サーバを分散させる場合には Memcached Plugin 等を利用してください。

ライセンス

このモジュールは Apache Software License, version 2 の元に公開します。

詳しくは LICENSE ファイルを参照ください。