[Scala][Play] EssentialActionによる認可処理の実装

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

PlayFrameworkではエンドポイントに対して Action を定義します。これはリクエストをうけつけ、レスポンスを返却する関数(非同期含む)を渡すことで構成できます。

def endpointAction = Action { request /* リクエスト */ => 
  Ok("some response") // レスポンス
}

外部クライアント向けに提供するAPIでは

  • ヘッダ等に付与されたアクセス元情報をモデルにコンバートしてその後の処理を行いやすくする
  • ヘッダ等に付与されたトークンを元に認可処理を行い、リクエストをフィルタリングする

要求が往々にして発生すると思われますが、そのために利用できる機能の一つとして EssentialAction があります。

環境情報

  • Scala 2.11.7
  • sbt 0.13.8
  • Playframework 2.4.6
  • Specs2

EssentialAction

EssentialActionは下記の形を持つtraitです

trait EssentialAction extends (RequestHeader => Iteratee[Array[Byte], Result]) with Handler {

  def apply() = this

}

このtraitは RequestHeader => Iteratee[Array[Byte], Result] を継承しています。 (=>Function1 の構文糖衣)

ここでの RequestHeader はHTTPリクエストヘッダを表すtraitです。

また、Iteratee[Array[Byte], Result] については非同期ストリーム処理を抽象化した Iteratee と言う型が用いられています。

この Iteratee の詳細については本記事の範囲を大きく超えるため、詳しく解説しません。もっと知りたい方には以下の資料が参考になると思われます。(ちなみに Play 2.5 系では Accumulator という同じような役割を持つ型に置き換わっています)

Iteratee[Array[Byte], Result] 自体はリクエストボディのバイト文字列を表す Array[Byte] を入力にとってレスポンスを表す Result 出力として返却する非同期ストリーム処理を表しています。Iteratee[E, A] は非同期を表すストリームという性質を持っており、run メソッドを用いれば型Eを入力とするストリームのAへのパースが終わった時点で完了するような非同期処理 Future[A] 型の返り値を得られます。

いま、ここでもう一度Actionの定義にもどってみます。

Actionは下記のように、リクエスト全体を引数にとり、レスポンスを返却する関数から構成可能でした。

def endpointAction = Action { request /* リクエスト */ => 
  Ok("some response") // レスポンス
}

レスポンスは非同期も許容するため、Actionは大雑把にいって以下のような変換を表すことが分かります。

Request => Future[Result]

リクエスト全体を表す型はPlayでは Request[A] (Aはボディを表す型) で表され、これはリクエストヘッダを有することから、RequestHeader のtraitを継承しています。

trait Request[+A] extends RequestHeader {

  def body: A
  ...
}

したがって、Actionは以下のように考えることもできます(Aはリクエストボディを表す型として)

(RequestHeader, A) => Future[Result]

カリー化してみれば次のような形に見立てることもできますが

RequestHeader => A => Future[Result]

AはボディだったのでArray[Byte]とみなせば

RequestHeader => Array[Byte] => Future[Result]

あれ、どこかで見たことある形ですね。EssentialActionの継承している関数 RequestHeader => Iteratee[Array[Byte], Result] と似ていませんか?

実は Action[A](Aはボディを表す型)はEssentialActionを継承しています。

つまり、いままでリクエストをハンドリングして非同期でレスポンスを返している処理

Request => Future[Result]

を表すActionは以下のように、リクエストヘッダをハンドリングして「リクエストボディのバイト文字列からレスポンスへ変換する非同期ストリーム処理」を返却する処理

RequestHeader => Iteratee[Array[Byte], Result]

を表すEssentialActionであるとみなすことができます。

EssentialActionの使用用途

リクエストハンドリングに用いるためのActionが汎用的なEssentialActionとみなせることがわかったところで、次にこれを用いてどのような処理を記述できるか見てみましょう。

EssentialActionはtraitですが、同名のobjectも存在し、そこには以下の様なapplyメソッドが定義されています。

object EssentialAction {

  def apply(f: RequestHeader => Iteratee[Array[Byte], Result]): EssentialAction = new EssentialAction {
    def apply(rh: RequestHeader) = f(rh)
  }
}

ここでやっている処理、結構わかりにくいのですが、traitを継承する新しい匿名クラスを宣言し、そこでFunction1のapplyを実装しています。

カスタムのEssentialActionを実装する際にはこのapplyメソッドを用いてカスタムのEssentialActionを作成します。

実際の実装でEssentialActionを使うケースはこのRequestHeaderから得られる情報を引き出す処理を予め匿名のEssentialAction継承クラスに記述してしまい、その匿名クラス生成時に渡す関数のハンドラを以下のような形にすることで

object InfoEssentialAction {

  def apply(f: Info => EssentialAction): EssentialAction = EssentialAction { requestHeader => // ここでapplyメソッドが使われる
    ...
  }
}

以下の様な形でヘッダから得られる情報をActionから容易に用いれるようにすることです。

def someAction = InfoEssentialAction { info =>
  Action { req =>
    Ok(s"${info.somefield}")
  }
}

EssentialActionを用いて認可処理のためのアクションを実装する

認可処理を行い、認可エラーだった時の処理をまとめて行ってくれ、かつ認可成功したときはユーザに関わる情報を提供してくれるカスタムの認可アクションをEssentialActionを用いて作ってみます。この認可処理に際して、外部で次のような認可サービスがある前提に立ちます。

package services

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

case class UserInfo(name: String, id: String, email: String) // --(1)

class AuthorizationService {

  /**
   * トークンを取得
   */
  def authorize(token: String): Future[UserInfo] = Future(UserInfo("aaa", "yguihl", "aaa")) // --(2)
}
  • --(1) ダミーのユーザ情報
  • --(2) 認可サービスの認可メソッド、ここではサンプルのため、ダミーデータを返却するようにします。

実装に際してはコントローラにこのサービスがある必要があるのですが、その要請をtraitで表現し、そのtraitの配下で以下のようにカスタムのアクションを宣言します。

package controllers

import play.api.libs.iteratee.{Done, Iteratee}
import play.api.mvc.{EssentialAction, Results}
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import services.{AuthorizationService, UserInfo}

trait UserInfoActionComponent {

  val authorizationService: AuthorizationService // --(1)

  val HeaderName = "Authorization" // --(2)

  object UserInfoAction {

    def apply(action: UserInfo => EssentialAction): EssentialAction = EssentialAction { requestHeader => // --(3)
      val maybeToken = requestHeader.headers.get(HeaderName) // --(4)
      maybeToken match {
        case None => Done(Results.Unauthorized) // --(5)
        case Some(token) => Iteratee.flatten { // --(6)
          authorizationService.authorize(token).map { // --(7)
            action(_)(requestHeader) // --(8)
          }
        }
      }
    }
  }
}
  • --(1) UserInfoActionを使いたいコントローラに対して認可サービスをインジェクトするように要請します。
  • --(2) 認可用のトークンが置かれるヘッダのキーです。
  • --(3) カスタムの認可アクション生成部、ユーザ情報をヘッダから読み出し、Actionから読み込めるようにします。
  • --(4) Authorizationヘッダの値をOption[String]で取得します。
  • --(5) ヘッダの中身がないときは401で返却します。ここで、Done.applyはIterateeのうち、ストリーム処理終了状態を表すDoneIterateeを生成するコンストラクタです。
  • --(6) ヘッダの中身がある時の処理を記述します。Iteratee.flatten はFuture[Iteratee[E, A]]をIteratee[E, A]に変換するメソッドです。
  • --(7) 認可サービスを呼び出し、成功時にマッピングして、Iterateeへの変換をします
  • --(8) Iterateeへの変換はメソッド引数のactionを通じて行います。UserInfo => EssentialActionUserInfo => RequestHeader => Interatee[Array[Byte], Result] とみなせ、EssentialAction.applyがメソッド引数として RequestHeader => Iteratee[Array[Byte], Result] を要求するため、Iteratee.flattenの中では返り値をFuture[Iteratee]にする必要があり、(_)でサービスから取得したUserInfoの適用を行い、得られたEssentialActionに対して (requestHeader) でリクエストヘッダ引数の適用を行うことでIterateeを得ています。

実際にControllerで用いるときは次のように認可サービスをインジェクトした上で、必要なTraitを継承します。

package controllers

import javax.inject._

import play.api.mvc._
import services.AuthorizationService

@Singleton
class HomeController @Inject()(
  override val authorizationService: AuthorizationService
) extends Controller with UserInfoActionComponent {

  def someAction = UserInfoAction { info =>
    Action {
      Ok(s"${info.name}")
    }
  }

}

UserInfoActionに既存のActionをネストするだけで認可処理が行われます。

EssentialActionのテスト

EssentialActionに対するテストはSpecs2のcallを通じて行いやすくなっています。コード例だけ掲載しますが、UserInfoActionを噛ませて認可処理ができていることが確認できると思います。

import controllers.UserInfoActionComponent
import org.specs2.mock.Mockito
import org.specs2.specification.Scope
import play.api.mvc.{Action, Results}
import play.api.test.{FakeRequest, PlaySpecification}
import services.{AuthorizationService, UserInfo}

import scala.concurrent.Future

/**
 * 認可アクションテスト
 */
class UserInfoActionSpec extends PlaySpecification with Mockito {

  trait TestComponent extends UserInfoActionComponent {
    val mockService = mock[AuthorizationService]
    override val authorizationService: AuthorizationService = mockService
  }

  trait CommonBefore {
    self: TestComponent =>
    val mockUserInfo = UserInfo("taro", "111111", "taro@email.com")
    mockService.authorize(anyString) returns Future.successful(mockUserInfo)
  }

  class CommonContext extends Scope with TestComponent with CommonBefore

  "リクエスト認可処理" should {

    "認可ヘッダがリクエストにある" should {

      class Context extends CommonContext {
        val mockRequest = FakeRequest().withHeaders(HeaderName -> "aaaa")
      }

      "リクエストが成功する" in new Context {
        val res = call(UserInfoAction { info =>
          Action(Results.Ok)
        }, mockRequest)
        status(res) mustEqual OK
      }

      "userInfoに正常な値が入っている" in new Context {
        val res = call(UserInfoAction { info =>
          Action(Results.Ok(s"${info.name}, ${info.id}, ${info.email}"))
        }, mockRequest)
        contentAsString(res) mustEqual s"${mockUserInfo.name}, ${mockUserInfo.id}, ${mockUserInfo.email}"
      }
    }

    "認可ヘッダがリクエストにない" should {

      class Context extends CommonContext {
        val mockRequest = FakeRequest()
      }

      "リクエストが失敗し、401を返却する" in new Context {
        val res = call(UserInfoAction { info =>
          Action(Results.Ok)
        }, mockRequest)
        status(res) mustEqual UNAUTHORIZED
      }
    }
  }
}

まとめ

いかがでしたでしょうか。APIのあらゆる箇所で使う認可処理等に代表される、ヘッダからの情報取得、コンバート、エラーフィルタリングを簡潔に実装できるコンポーネントをEssentialActionを通じて実現できることがおわかりいただけたかと思います。

本記事で紹介した認可処理以外にも、UAヘッダのパース処理、セッションやクッキーの管理でEssentialActionは効力を発揮します。

ぜひお試しください!