简介
本文介绍使用Spray实现基本的Http用户验证.
实现方式
实现用户验证的两个处理部分:
- Authentication:对用户的凭据进行验证(账号密码或SSL客户端证书),并创建一些类似的对象携带用户的信息.
- Authorization:使用该对象中包含的数据验证用户的操作权限.
首先,使用一个ApiUser类来存储用户的登录账号和密码,或者另外存储一些类似用户名和Email的数据.然后使用scala-bcrypt来hash用户的密码,然后添加一个withPassword方法用于设置密码,一个passwordMatches方法用于检查给出的密码是否与用户密码匹配.hashedPassword是一个Option[String],因此可以设置为None来表示未登录的用户.
import com.github.t3hnar.bcrypt._
import org.mindrot.jbcrypt.BCrypt
case class ApiUser(login: String, hashedPassword: Option[String] = None) {
def withPassword(password: String) = copy (hashedPassword = Some(password.bcrypt(generateSalt)))
def passwordMatches(password: String): Boolean = hashedPassword.exists(hp => BCrypt.checkpw(password, hp))
}
另外创建一个类用于存储已登录用户的信息,以便验证用户执行操作的权限.使用一个case class从用户授权中存储用户的信息:
class AuthInfo(val user: ApiUser) {
def hasPermission(permission: String) = {
// Code to verify whether user has the given permission }
}
现在只是用一个字符串来定义用户权限,当然实际上并不推荐这么做.
然后继续身份验证步骤.根据RFC 2617,当使用HTTP基本身份验证时客户端必须发送一个授权标头,如下:
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Basic后面跟的是userId和password,使用冒号分割并且使用base64编码.如果不提供Authorization头或者给出的userId和password不正确,服务器会返回一个401 Unauthorized状态吗.Spray提供了BasicAuth类来处理所有的自动身份验证指令结合.
在Spray文档中,authenticate指令可以接收一个Future[Authentication[T]]或者ContextAuthenticator[T].一个ContextAuthenticator方法用于接收一个RequestContext(其中包含所有请求信息)然后返回一个Future[Authentication[T]].由于我们需要请求数据进行用户验证,所以需要使用第二种方式.
Spray提供一个继承自Authentication[T]的类BasicAuth,同时从Authorization头中解析账户和密码,但是我们需要提供一个功能用于自动用户验证然后返回一个Future[Option[T]].在我们的用例中,T是AuthInfo类,因此当账户密码有效时它需要返回一个包含Some(authInfo)的Future,无效则返回None.
创建一个特质来实现这样的功能,在路由或运行路由的Actor中使用会非常灵活:
trait Authenticator {
def basicUserAuthenticator(implicit ec: ExecutionContext): AuthMagnet[AuthInfo] = {
def validateUser(userPass: Option[UserPass]): Option[AuthInfo] = {
for {
p <- userPass
user <- Repository.apiUsers(p.user)
if user.passwordMatches(p.pass)
} yield new AuthInfo(user)
}
def authenticator(userPass: Option[UserPass]): Future[Option[AuthInfo]] = future { validateUser(userPass) }
BasicAuth(authenticator _, realm = "Private API")
}
}
在我程序中,Repository.apiUsers对象包含一个apply方法用于接收用户的登录信息然后返回一个Option[ApiUser],你可以根据你应用的需要替换为一个Slick查询或者请求一个LDAP服务.Spray中一个比较好的特性是所有事物都是Future,这些请求都会以异步的方式进行.
这时,就可以在运行路由的Actor中混入这个特质,然后在需要用户登录的地方使用authenticator命令,authenticator会将一个由validateUser方法生成的AuthInfo对象当做参数发送到它自己的闭包,然后你就可以在路由中需要的地方进行使用.
假如你需要更细粒度的用户权限验证有怎么办呢? 比如允许所有的用户GET一个特别的URI,但是只允许部分用户使用PUT和POST,这就是authorize命令的用途了.authorize命令接收一个名称,如果有对应动作的权限则返回true,否则为false,可以使用authInfo进行验证.
authenticate(basicUserAuthenticator) { authInfo =>
path("private") {
get {
// All authenticated users can enter here
complete(s"Hi, ${authInfo.user.login}")}
post {
authorize(authInfo.hasPermission("post") {
// Only those users that have the "post" permission will be allowed in here}
}
}
}
如果用户验证失败,Spray会自动返回”403 Forbidden”状态码.
参考文档
Mario Camou - Implementing HTTP Basic Authentication With Spray