4

我正在构建一个连接到 Google Drive 的游戏网络应用程序。通过 Google OAuth 2.0 流程,当用户登录时,我将 access_token 保存到缓存中,并将 refresh_token(连同其他用户数据)保存到数据库和缓存中。Google OAuth accessTokens 仅持续1 小时,同样我缓存中的 accessToken 将在一小时后过期。

因此,接下来,我按照另一种方式创建一个经过验证的操作创建了一个经过验证的函数,除了用户之外,我还存储了 accessToken。

但是,accessToken 在一个小时后过期,如果它已经过期,那么我需要使用我的 refresh_token 向 google 发出 Web 服务 GET 请求,以便检索另一个 access_token。

我设法创建了一个看起来有点难看但有效的同步版本。我想知道是否有办法重新工作以使其同步?

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())))
  }
}

requestNewAccessToken向 Google 发出 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 文档(上图)中,我看不到如何将 AsyncResult 转换回响应而不将其重定向到第二个路由器。我想到的另一种可能性是拦截请求的过滤器,如果缓存中的 accessToken 已过期,只需从 Google 获取另一个并在 Action 方法开始之前将其保存到缓存中(这样,accessToken 将始终是最新的)。

正如我所说,同步版本有效,如果这是实现此过程的唯一方法,那就这样吧,但我希望可能有一种方法可以异步执行此操作。

太感谢了!

Play 2.2.0 更新

async {}Play 2.2.0 已弃用,并将在 Play 2.3 中删除。因此,如果您使用的是当前版本的 Play,则需要修改上面列出的解决方案。

提醒一下,逻辑上,当用户成功登录时,Google access_token 会被持久化到缓存中。access_token 只持续一个小时,所以我们在一小时后从缓存中删除 access_token。

因此,它的逻辑Authenticated是检查请求中是否存在 userId cookie。User然后它使用该 userId从缓存中获取匹配项。User包含一个refresh_token以防当前已access_token过期。如果缓存中没有userIdcookie 或者我们无法user从缓存中检索到匹配项,那么我们将启动一个新会话并重定向到应用程序登录页面。

如果用户成功从缓存中检索,那么我们尝试从缓存中获取access_token。如果它在那里,那么我们创建一个WrappedRequest包含requestuser和的对象access_token。如果它不在缓存中,那么我们向 Google 进行 Web 服务调用,获取一个新的access_token,它被持久化到缓存中,然后传递给WrappedRequest

要使用 发出异步请求Authenticated,我们只需添加.apply(与 相同Action),如下所示:

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

这是更新后的 trait,适用于 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

该方法返回 AsyncResult,它是 Result 的子类。这意味着我们需要完成在 Future 中生成正常结果的工作。然后我们可以将未来的结果传递给这个方法,在我们的 action 方法中返回它,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

好吧,我提出了两个答案,我需要感谢@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
  }
}
}

第二种选择是将所有东西放在一起作为一个大的平面地图/地图。

  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 作为隐式。我宁愿不重复重定向到 index() 两次,但 AsyncResult 不允许这样做。

persistAccessToken()返回 a Future[Option[String]],所以如果我尝试将它映射到 a 之外AsyncResult,它会给我一个Option[String](如果你将 Future 视为一个容器,这很有意义),所以它必须AsyncResult添加,这意味着我必须提供一个getOrElse以防万一persistAccessToken(将访问令牌保存到缓存并返回它的副本以供使用)......但这意味着我需要在代码中进行两次重定向。

如果有人知道更好的方法来做到这一点,我很乐意看到它。

于 2013-09-15T09:06:19.400 回答