【发布时间】:2013-09-24 06:58:13
【问题描述】:
我正在构建一个连接到 Google 云端硬盘的 Play 网络应用。通过 Google OAuth 2.0 流程,当用户登录时,我将 access_token 保存到缓存中,并将 refresh_token(连同其他用户数据)保存到数据库和缓存中。 Google OAuth accessTokens 只持续1 hour,同样我缓存中的 accessToken 会在一小时后过期。
所以,接下来,我按照Another way to create an Authenticated action 的方式创建了一个Authenticated 函数,除了用户之外,我还存储了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 post 请求,将 refreshToken 和其他东西一起发送,作为回报,google 会发回一个新的 Access Token,方法如下:
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。然后它使用该 userId 从缓存中获取匹配的User。 User 包含一个refresh_token,以防当前access_token 已过期。如果缓存中没有userId cookie,或者我们无法从缓存中检索到匹配的user,那么我们将启动一个新会话并重定向到应用程序登录页面。
如果用户成功从缓存中检索,那么我们尝试从缓存中获取access_token。如果存在,那么我们创建一个包含request、user 和access_token 的WrappedRequest 对象。如果不在Cache中,那么我们调用Google获取一个新的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
}
}
}
【问题讨论】:
标签: scala playframework-2.0 google-api google-drive-api