我正在尝试向我的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 {
有人请清除混乱,并可能帮助解决一些实施细节吗?
非常感谢
答案 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))
}