4

Google ドライブに接続するプレイ Web アプリを構築しています。Google OAuth 2.0 プロセスを経て、ユーザーがログインすると、access_token をキャッシュに保存し、refresh_token を (他のユーザー データと共に) データベースとキャッシュに保存します。Google OAuth accessTokens は1 時間しか持続せず、同様にキャッシュ内の accessToken は 1 時間で期限切れになります。

したがって、次のように、認証済みアクションを作成する別の方法の行に沿って認証済み関数を作成しましたが、ユーザーに加えて、accessToken も保存します。

ただし、accessToken は 1 時間後に有効期限が切れます。有効期限が切れている場合は、別の access_token を取得するために、refresh_token を使用して Google に Web サービス GET リクエストを実行する必要があります。

少し醜いように見えますが、機能する同期バージョンを作成することができました。同期するように再加工する方法があるのではないかと思っていましたか?

def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = {
  Action(p) { request =>
    val result1 = for {
      userId <- request.session.get(username)
      user <- Cache.getAs[User](s"user$userId")
      token <- Cache.getAs[String](accessTokenKey(userId))
    } yield f(AuthenticatedRequest(user, token, request))

    import scala.concurrent.duration._
    lazy val result2 = for {
      userId <- request.session.get(username)
      user <- Cache.getAs[User](s"user$userId")
      token <- persistAccessToken(Await.result(requestNewAccessToken(user.refreshToken)(userId), 10.seconds))(userId)
    } yield f(AuthenticatedRequest(user, token, request))

    result1.getOrElse(result2.getOrElse(Results.Redirect(routes.Application.index())))
  }
}

requestNewAccessTokenGoogle に WS ポスト リクエストを送信し、refreshToken を他のものと一緒に送信します。その見返りとして、Google は新しいアクセス トークンを返します。メソッドは次のとおりです。

def refreshTokenBody(refreshToken: String) = Map(
  "refresh_token" -> Seq(refreshToken),
  "client_id" -> Seq(clientId),
  "client_secret" -> Seq(clientSecret),
  "grant_type" -> Seq(tokenGrantType)
)

def requestNewAccessToken(refreshToken: String)(implicit userId: String): Future[Response] =
  WS.url(tokenUri).withHeaders(tokenHeader).post(refreshTokenBody(refreshToken))

Future[ws.Response] を ws.Response に変換する他の唯一の方法は onComplete を使用することですが、それは戻り値の型が Unit のコールバック関数であり、提供された例とうまく適合していないようです。 Playframework docs (上記) で、2 番目のルーターにリダイレクトせずに AsyncResult を応答に変換する方法がわかりません。私が考えたもう 1 つの可能性は、リクエストをインターセプトするフィルターです。キャッシュ内の accessToken の有効期限が切れている場合は、アクション メソッドが開始する前に、Google から別のトークンを取得してキャッシュに保存します (そうすれば、accessToken は常に最新であること)。

私が言ったように、同期バージョンは機能します。それがこの手順を実装する唯一の方法である場合は、それもそうですが、非同期でこれを行う方法があるかもしれないことを望んでいました.

どうもありがとうございます!

Play 2.2.0 のアップデート

async {}は Play 2.2.0 で非推奨となり、Play 2.3 で削除されます。そのため、現在のバージョンの Play を使用している場合は、上記のソリューションを修正する必要があります。

ユーザーが正常にログインすると、Google の access_token がキャッシュに永続化されます。access_token は 1 時間しか持続しないため、1 時間後にキャッシュから access_token を削除します。

したがって、のロジックはAuthenticated、リクエストに userId Cookie があるかどうかを確認することです。次に、その userId を使用して、一致するUserものをキャッシュからフェッチします。現在の有効期限が切れた場合に備えて、 にUserは が含まれます。キャッシュに Cookieがない場合、またはキャッシュから一致する Cookie を取得できない場合は、新しいセッションを開始し、アプリケーションのランディング ページにリダイレクトします。refresh_tokenaccess_tokenuserIduser

ユーザーがキャッシュから正常に取得された場合、キャッシュからの取得を試みaccess_tokenます。そこにある場合は、 、、およびWrappedRequestを含むオブジェクトを作成します。キャッシュにない場合は、Google への Web サービス呼び出しを行い、新しいを取得します。これはキャッシュに保持されてから、requestuseraccess_tokenaccess_tokenWrappedRequest

を使用して非同期リクエストを作成するには、次のように( の場合と同じ)Authenticatedを追加するだけです。.applyAction

def testing123 = Authenticated.async {
  Future.successful { Ok("testing 123") }
}

Play 2.2.0 で動作する更新されたトレイトは次のとおりです。

import controllers.routes
import models.User

import play.api.cache.Cache
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.mvc._
import play.api.Play.current
import scala.concurrent.Future

trait Authenticate extends GoogleOAuth  {

  case class AuthenticatedRequest[A](user: User, accessToken: String, request: Request[A])
    extends WrappedRequest[A](request)

  val startOver: Future[SimpleResult] = Future {
    Results.Redirect(routes.Application.index()).withNewSession
  }

  object Authenticated extends ActionBuilder[AuthenticatedRequest] {
    def invokeBlock[A](request: Request[A],
                   block: (AuthenticatedRequest[A] => Future[SimpleResult])) = {
      request.session.get(userName).map { implicit userId =>
        Cache.getAs[User](userKey).map { user =>
          Cache.getAs[String](accessTokenKey).map { accessToken =>
            block(AuthenticatedRequest(user, accessToken, request))
          }.getOrElse { // user's accessToken has expired, so do WS call to Google for another one
            requestNewAccessToken(user.token).flatMap { response =>
              persistAccessToken(response).map { accessToken =>
                block(AuthenticatedRequest(user, accessToken, request))
              }.getOrElse(startOver)
            }
          }
        }.getOrElse(startOver) // user not found in Cache
      }.getOrElse(startOver) // userName not found in session
    }
  }
}
4

2 に答える 2

3

http://www.playframework.com/documentation/2.1.3/ScalaAsyncを見ると 、Async メソッドを使用するように指示されています。署名を見ると、魔法がどのように機能するかがわかります。

def Async(promise : scala.concurrent.Future[play.api.mvc.Result]) : play.api.mvc.AsyncResult

このメソッドは、Result のサブクラスである AsyncResult を返します。つまり、Future 内で通常の Result を生成する作業を行う必要があります。その後、将来の結果をこのメソッドに渡し、それをアクション メソッドで返すことができます。あとは Play が処理します。

def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = {
    request => {
        case class UserPair(userId: String, user: User)

        val userPair: Option[UserPair] = for {
            userId <- request.session.get(username)
            user <- Cache.getAs[User](s"user$userId")
        } yield UserPair(userId, user)

        userPair.map { pair =>
            Cache.getAs[String](accessTokenKey(pair.userId)) match {
                case Some(token) => f(AuthenticatedRequest(pair.user, token, request))
                case None => {
                    val futureResponse = requestNewAccessToken(pair.user.refreshToken)(pair.userId)
                    Async {
                        futureResponse.map {response =>
                            persistAccessToken(response)(pair.userId) match {
                                case Some(token) => f(AuthenticatedRequest(pair.user, token, request))
                                case None => Results.Redirect(routes.Application.index())
                            }
                        }
                    }
                }
            }
        }.getOrElse(Results.Redirect(routes.Application.index()))
    }
}
于 2013-09-14T16:47:31.980 に答える
1

まあ、私は2つの答えを提示します.@Karlにクレジットを与える必要があります.(彼の答えはコンパイルされませんでしたが)、彼は私を正しい方向に向けました:

プロセスをチャンクに分割するバージョンを次に示します。

  def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = {
Action(p) { request => {

  val userTuple: Option[(String, User)] =
    for {
      userId <- request.session.get(userName)
      user <- Cache.getAs[User](userKey(userId))
    } yield (userId, user)

  val result: Option[Result] =
    for {
      (userId, user) <- userTuple
      accessToken <- Cache.getAs[String](accessTokenKey(userId))
    } yield f(AuthenticatedRequest(user, accessToken, request))

  lazy val asyncResult: Option[AsyncResult] = userTuple map { tuple =>
    val futureResponse = requestNewAccessToken(tuple._2.token)(tuple._1)
    AsyncResult {
      futureResponse.map { response => persistAccessToken(response)(tuple._1).map {accessToken =>
        f(AuthenticatedRequest(tuple._2, accessToken, request))
      }.getOrElse { Results.Redirect(routes.Application.index()).withNewSession }
      }
    }
  }

  result getOrElse asyncResult.getOrElse {
    Results.Redirect(routes.Application.index()).withNewSession
  }
}
}

2 番目のオプションは、すべてを 1 つの大きなフラットマップ/マップとしてまとめることです。

  def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = {
Action(p) { request => {

  val result = request.session.get(userName).flatMap { implicit userId =>
    Cache.getAs[User](userKey).map { user =>
      Cache.getAs[String](accessTokenKey).map { accessToken =>
        f(AuthenticatedRequest(user, accessToken, request))
      }.getOrElse {
        val futureResponse: Future[ws.Response] = requestNewAccessToken(user.token)
        AsyncResult {
          futureResponse.map { response => persistAccessToken(response).map { accessToken =>
            f(AuthenticatedRequest(user, accessToken, request))
          }.getOrElse { Results.Redirect(routes.Application.index()).withNewSession}}
        }
      }
    }
  }

  result getOrElse Results.Redirect(routes.Application.index()).withNewSession
}}}

userId を暗黙的に使用できるため、2 番目のバージョンを少し好みます。index() へのリダイレクトを 2 回重複させたくないのですが、AsyncResult はそれを許可しません。

persistAccessToken()を返すので、それをFuture[Option[String]]の外にマッピングしようとすると(Future をコンテナーとして見れば意味があります) が返されるので、 を追加する必要があります。つまり、 を提供する必要があります。場合に備えて(アクセストークンをキャッシュに保存し、使用するためにそのコピーを返します)...しかし、これはコードに2つのリダイレクトが必要であることを意味します.AsyncResultOption[String]AsyncResultgetOrElsepersistAccessToken

誰かがこれを行うためのより良い方法を知っているなら、私はそれを見てみたい.

于 2013-09-15T09:06:19.400 に答える