在单页应用中播放框架身份验证

Play Framework Authentication in a single page app

我正在尝试向我的 Play Framework 单页应用程序添加身份验证。

我想要的是:

def unsecured = Action {
    Ok("This action is not secured")
}

def secured = AuthorizedAction {
    // get the authenticated user's ID somehow
    Ok("This action is secured")
}

对于传统的网络应用程序,我之前按照 Play Framework 文档这样做过:

def authenticate = Action { implicit request =>
  loginForm.bindFromRequest.fold(
    formWithErrors => BadRequest(views.html.login(formWithErrors)),
    user => {
      Redirect(routes.Application.home).withSession(Security.username -> user._1)
    }
  )
}

def logout = Action {
  Redirect(routes.Auth.login).withNewSession.flashing(
    "success" -> "You are now logged out."
  )
}

并且授权操作正在扩展 ActionBuilder,如下所示:

object AuthorizedAction extends ActionBuilder[Request] with Results {

  /**
   * on auth success: proceed with the request
   * on auth failure: redirect to login page with flash
   */
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    // TODO: is "isDefined" enough to determine that user is logged in?
    if(request.session.get("username").isDefined) {
      block(request)
    }
    else {
      Future.successful(Redirect(routes.Auth.login).flashing(
        "failure" -> "You must be logged in to access this page."
      ))
    }
  }
}

但是对于单页应用程序,这种方法不再有效。

James Ward 的这篇文章解释了如何设计新方法,并包括 Java 实现: Securing SPA and rest services

该实现由 Marius Soutier 在 Scala 中重做:Securing SPA in Scala

在他的示例中,他实现了安全特性:

trait Security { self: Controller =>

  val cache: CacheApi

  val AuthTokenHeader = "X-XSRF-TOKEN"
  val AuthTokenCookieKey = "XSRF-TOKEN"
  val AuthTokenUrlKey = "auth"

  /** Checks that a token is either in the header or in the query string */
  def HasToken[A](p: BodyParser[A] = parse.anyContent)(f: String => Long => Request[A] => Result): Action[A] =
    Action(p) { implicit request =>
      val maybeToken = request.headers.get(AuthTokenHeader).orElse(request.getQueryString(AuthTokenUrlKey))
      maybeToken flatMap { token =>
        cache.get[Long](token) map { userid =>
          f(token)(userid)(request)
        }
      } getOrElse Unauthorized(Json.obj("err" -> "No Token"))
    }

}

函数现在是这样保护的,而不是简单的操作:

def ping() = HasToken() { token => userId => implicit request =>
  user.findByID (userId) map { user =>
    Ok(Json.obj("userId" -> userId)).withToken(token -> userId)
  } getOrElse NotFound (Json.obj("err" -> "User Not Found"))
}

其中 .withToken 定义为:

implicit class ResultWithToken(result: Result) {
  def withToken(token: (String, Long)): Result = {
    cache.set(token._1, token._2, CacheExpiration)
    result.withCookies(Cookie(AuthTokenCookieKey, token._1, None, httpOnly = false))
  }

  def discardingToken(token: String): Result = {
    cache.remove(token)
    result.discardingCookies(DiscardingCookie(name = AuthTokenCookieKey))
  }
}

我不喜欢上面的 "ping" 函数变得如此复杂,并且更愿意使用 Action Builder(如第一个示例),在其中捕获并处理身份验证失败. (截至目前,如果我想保护 ping2 和 ping3 功能,每个功能都必须检查是否找到用户并处理 "not found" 案例)

我试图将一个动作生成器放在一起,灵感来自 Marius 的实现,尤其是他对 cacheApi 的使用,这是必要的。

但是AuthorizedAction是一个对象,需要注入cacheApi(所以需要把对象改成单例class),或者在没有定义的情况下不能在对象中声明。

我也觉得 AuthorizedAction 需要保留一个对象,以便用作:

def secured = AuthorizedAction {

任何人都可以消除混淆,并可能帮助解决一些实施细节问题吗?

非常感谢

我认为最简单的方法是使用 ActionBuilder。您可以将动作生成器定义为 class(并向其传递一些依赖项)或对象。

首先,您需要定义一个包含用户信息的请求类型:

// You can add other useful information here
case class AuthorizedRequest[A](request: Request[A], user: User) extends WrappedRequest(request)

现在定义你的ActionBuilder

class AuthorizedAction(userService: UserService) extends ActionBuilder[AuthorizedRequest] {

  override def invokeBlock[A](request: Request[A], block: (AuthorizedRequest[A]) ⇒ Future[Result]): Future[Result] = {
    request.headers.get(AuthTokenHeader).orElse(request.getQueryString(AuthTokenUrlKey)) match {
      case Some(token) => userService.findByToken(token).map {
        case Some(user) =>
          val req = AuthorizedRequest(request, user)
          block(req)
        case None => Future.successful(Results.Unauthorized)
      }
      case None => Future.successful(Results.Unauthorized)
    }

  }
}

现在您可以在您的控制器中使用它了:

val authorizedAction = new AuthorizedAction(userService)
def ping = authorizedAction { request =>
  Ok(Json.obj("userId" -> request.user.id))
}