我正在构建一个连接到Google云端硬盘的Play网络应用程序。通过Google OAuth 2.0过程,当用户登录时,我将access_token保存到缓存中,并将refresh_token(以及其他用户数据)保存到数据库和缓存中。Google OAuth accessTokens 只能持续 1 小时,同样,我缓存中的 accessToken 也会在一小时内过期。
因此,接下来,我按照另一种创建身份验证操作的方法创建了一个经过身份验证的函数,除了用户之外,我还存储了 accessToken。
但是,accessToken 会在一小时后过期,如果它已过期,那么我需要用我的refresh_token向谷歌发出网络服务 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发回一个新的访问令牌,方法是:
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))
这似乎是转换未来的唯一其他方法[ws.响应]对 ws。响应是使用 onComplete,但这是一个返回类型为 Unit 的回调函数,它似乎与 Playframework 文档(上面)中提供的示例不太吻合,而且我不明白如何将 AsyncResult 转换回响应而不将其重定向到第二个路由器。我想到的另一种可能性是拦截请求的过滤器,如果缓存中的访问令牌已过期,只需从Google获取另一个并在Action方法启动之前将其保存到缓存中(这样,accessToken将始终是最新的)。
正如我所说,同步版本有效,如果这是实现此过程的唯一方法,那就这样吧,但我希望可能有一种方法可以异步执行此操作。
非常感谢!
播放 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
对象。如果它不在缓存中,那么我们向 Google 进行网络服务调用,获取一个新的access_token
,该持久化到缓存中,然后传递给WrappedRequest
要使用 Authenticated
发出异步请求,我们只需添加.apply
(与Action
相同),如下所示:
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
}
}
}
如果你看看 http://www.playframework.com/documentation/2.1.3/ScalaAsync系统会告诉您使用异步方法。 当您查看签名时,您将看到魔术是如何工作的:
def Async(promise : scala.concurrent.Future[play.api.mvc.Result]) : play.api.mvc.AsyncResult
该方法返回 AsyncResult,它是 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()))
}
}
好吧,我提出了两个答案,我需要赞扬@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()
返回一个Future[Option[String]]
,所以如果我尝试在AsyncResult
之外映射它,它会给我一个Option[String]
(如果你将 Future 视为容器,这是有意义的),所以,它必须添加AsyncResult
,这意味着我必须提供一个getOrElse
以防万一persistAccessToken
(将访问令牌保存到缓存并返回它的副本以供使用)......但这意味着我需要在代码中有两个重定向。
如果有人知道更好的方法,我很想看到它。