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

时间:2016-10-05 04:50:41

标签: scala authentication playframework playframework-2.0

我正在尝试向我的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"))
    }

}

现在可以像这样保护函数,而不是简单的Action:

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(如第一个示例),其中auth失败被捕获并在单个点处理。 (截至目前,如果我想保护ping2和ping3的功能,每个人都必须检查用户是否被找到并处理"未找到" case)

我试图组建一个动作建设者,灵感来自Marius'实现,最特别是他使用cacheApi是必要的。

然而,AuthorizedAction是一个对象,需要注入cacheApi(因此需要将对象更改为singleton类),或者无法在未定义的情况下在对象中声明。

我还觉得AuthorizedAction需要保留为对象,以便用作:

def secured = AuthorizedAction {

有人请清除混乱,并可能帮助解决一些实施细节吗?

非常感谢

1 个答案:

答案 0 :(得分:1)

我认为最简单的方法是使用ActionBuilder。您可以将操作构建器定义为类(并将其传递给某些依赖项)或作为对象。

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

// 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))
}