我已经编写了一个基于Play with Scala的API,我对结果非常满意。我正处于一个阶段,我正在考虑优化和重构下一版API的代码,我有几个问题,其中最紧迫的是身份验证和我管理身份验证的方式。 / p>
我写的产品与企业有关,所以在每个请求中公开用户名+密码,或者在服务器端维护会话都不是最好的选择。以下是我的应用程序验证的工作原理: 用户使用用户名/密码进行身份验证 服务器返回与用户关联的令牌(存储为用户表中的列) 从该点向服务器发出的每个请求都必须包含令牌。 用户注销时也会更改令牌,也会定期更改令牌。 现在,我的实现非常简单 - 我有几个表单用于所有API端点,每个端点都需要一个令牌来查找用户,然后评估是否允许用户在请求中进行更改,或获取数据。因此,经过身份验证的领域中的每个表单都是需要令牌的表单,然后是其他几个参数,具体取决于API端点。
这导致重复。我使用的每个表单都必须具有基于令牌的验证部分。它显然不是最简单的方法。我一直需要一遍又一遍地复制相同类型的代码。
我一直在阅读有关Play过滤器的内容并提出一些问题: 使用Play过滤器进行基于令牌的身份验证是个好主意 过滤器是否可以应用于某个请求路径? 如果我根据过滤器中提供的令牌查找用户,是否可以将查找的用户对象传递给操作,以便我们不会重复查找该请求? (参见我如何处理这种情况的例子。)
case class ItemDelete(usrToken: String, id: Long) {
var usr: User = null
var item: Item = null
}
val itemDeleteForm = Form(
mapping(
"token" -> nonEmptyText,
"id" -> longNumber
) (ItemDelete.apply)(ItemDelete.unapply)
verifying("unauthorized",
del => {
del.usr = User.authenticateByToken(del.usrToken)
del.usr match {
case null => false
case _ => true
}
}
)
verifying("no such item",
del => {
if (del.usr == null) false
Item.find.where
.eq("id", del.id)
.eq("companyId", del.usr.companyId) // reusing the 'usr' object, avoiding multiple db lookups
.findList.toList match {
case Nil => false
case List(item, _*) => {
del.item = item
true
}
}
}
)
)
答案 0 :(得分:3)
查看Action Composition,它允许您检查和转换操作请求。如果您使用播放过滤器,那么它将在每个请求上运行。
例如,您可以创建一个检查请求的TokenAction,如果找到了令牌,则优化请求以包含基于令牌的信息,例如用户。如果没有找到令牌,则返回另一个结果,如Unauthorized,Redirect或Forbidden。
我创建了一个SessionRequest,它具有一个带有可选登录用户的用户属性,它首先从数据库中查找现有会话,然后获取附加用户并将其传递给请求
如果没有可用的用户,则过滤器(WithUser)将拦截SessionRequest,然后将用户重定向到登录页面
// Request which optionally has a user
class SessionRequest[A](val user: Option[User], request: Request[A]) extends WrappedRequest[A](request)
object SessionAction extends ActionBuilder[SessionRequest] with ActionTransformer[Request, SessionRequest] {
def transform[A](request: Request[A]): Future[SessionRequest[A]] = Future.successful {
val optionalJsonRequest: Option[Request[AnyContent]] = request match {
case r: Request[AnyContent] => Some(r)
case _ => None
}
val result = {
// Check if token is in JSON request
for {
jsonRequest <- optionalJsonRequest
json <- jsonRequest.body.asJson
sessionToken <- (json \ "auth" \ "session").asOpt[String]
session <- SessionRepository.findByToken(sessionToken)
} yield session
} orElse {
// Else check if the token is in a cookie
for {
cookie <- request.cookies.get("sessionid")
sessionToken = cookie.value
session <- SessionRepository.findByToken(sessionToken)
} yield session
} orElse {
// Else check if its added in the header
for {
header <- request.headers.get("sessionid")
session <- SessionRepository.findByToken(header)
} yield session
}
result.map(x => new SessionRequest(x.user, request)).getOrElse(new SessionRequest(None, request))
}
}
// Redirect the request if there is no user attached to the request
object WithUser extends ActionFilter[SessionRequest] {
def filter[A](request: SessionRequest[A]): Future[Option[Result]] = Future.successful {
request.user.map(x => None).getOrElse(Some(Redirect("http://website/loginpage")))
}
}
然后您可以在动作中使用它
def index = (SessionAction andThen WithUser) { request =>
val user = request.user
Ok("Hello " + user.name)
}
我希望这能让您了解如何使用动作合成
答案 1 :(得分:1)
Stormpath的人员有一个示例Play应用程序,通过他们的后端服务提供身份验证。它的一些代码可能对您有用。
它使用用户名/密码而不是令牌,但修改它不应该很复杂。
他们已遵循此Play文档: https://www.playframework.com/documentation/2.0.8/ScalaSecurity
具体实现如下: https://github.com/stormpath/stormpath-play-sample/blob/dev/app/controllers/MainController.scala
此Controller
处理身份验证操作,并通过isAuthenticated
Trait(依赖Secured
)提供play.api.mvc.Security
操作。此操作检查用户是否
如果他不是,则进行身份验证并将其重定向到登录屏幕:
/**
* Action for authenticated users.
*/
def IsAuthenticated(f: => String => Request[AnyContent] => Future[SimpleResult]) =
Security.Authenticated(email, onUnauthorized) { user =>
Action.async { request =>
email(request).map { login =>
f(login)(request)
}.getOrElse(Future.successful(onUnauthorized(request)))
}
}
然后,需要经过身份验证的操作的每个控制器必须使用 安全特质:
object MyController extends Controller with Secured
这些操作被IsAuthenticated操作“包裹”:
def deleteItem(key: String) = IsAuthenticated { username => implicit request =>
val future = Future {
MyModel.deleteItem(request.session.get("id").get, key)
Ok
}
future.map(
status => status
)
}
请注意,deleteItem
操作不需要用户名,只需要key
。但是,会话中会自动获取身份验证信息。因此,业务'API不会受到特定于安全性的参数的污染。