我正在构建一个连接到 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
过期。如果缓存中没有userId
cookie 或者我们无法user
从缓存中检索到匹配项,那么我们将启动一个新会话并重定向到应用程序登录页面。
如果用户成功从缓存中检索,那么我们尝试从缓存中获取access_token
。如果它在那里,那么我们创建一个WrappedRequest
包含request
、user
和的对象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
}
}
}