【问题标题】:Playframework scala, how to asynchronous convert a web service call to a response without AsyncPlayframework scala,如何在没有异步的情况下将 Web 服务调用异步转换为响应
【发布时间】: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 从缓存中获取匹配的UserUser 包含一个refresh_token,以防当前access_token 已过期。如果缓存中没有userId cookie,或者我们无法从缓存中检索到匹配的user,那么我们将启动一个新会话并重定向到应用程序登录页面。

如果用户成功从缓存中检索,那么我们尝试从缓存中获取access_token。如果存在,那么我们创建一个包含requestuseraccess_tokenWrappedRequest 对象。如果不在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


    【解决方案1】:

    如果你看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()))
        }
    }
    

    【讨论】:

    • 您的解决方案在 Play 2.1.x 中运行良好,但在 Play 2.2.x 中已弃用 async{}。我发布了对我的问题的编辑,显示了与 Play 2.2.0 中的Action.async 一致的解决方案
    【解决方案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
      }
    }
    }
    

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

      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(它将访问令牌保存到缓存并返回它的副本以供使用)......但这意味着我需要在代码中进行两个重定向。

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

    【讨论】:

    • 出于好奇,我没有编译什么?此外,您的第一个选项(没有将 asynchResult 声明为惰性)将每次都调用您的网络服务。您知道您不仅可以在映射正确时声明隐式吗?所以你可以写:“implicit val userId = userTuple._1”。
    • 另外,您无法避免这两个重定向。一个适用于您没有用户标识或用户的情况。另一种是如果您的 Web 服务调用失败(您正在异步执行此操作,因此它不在同一范围内)。
    • @Karl,实际上,变化很小,将主体的开头更改为Action(p) request =&gt; {,将Async 更改为AsyncResult(这是一个特征,而不是控制器)。总的来说,这是一篇内容丰富的帖子,我学到了很多东西,谢谢你的帮助!
    猜你喜欢
    • 2016-02-24
    • 2019-12-16
    • 1970-01-01
    • 1970-01-01
    • 2018-02-23
    • 2018-03-11
    • 2023-03-15
    • 2011-09-03
    相关资源
    最近更新 更多