Vapor 大型教程
用户认证概述
认证是确认“你是谁”的过程。本文带你给 TIL 应用加上密码哈希、用户名唯一约束,并让接口在返回时不再泄露密码。
认证与授权的区别
认证(Authentication)是验证某个人是否合法的过程。它和授权(Authorization)不同:授权是赋予已登录用户执行某个动作的权力。TIL 应用也遵循常见做法,用“用户名 + 密码”的组合来做认证。
先在 User 模型里增加 password 字段,并修改初始化方法:
final class User: Codable {
var id: UUID?
var name: String
var username: String
var password: String
init(name: String, username: String, password: String) {
self.name = name
self.username = username
self.password = password
}
}
用 BCrypt 哈希存储密码
存储密码有一条铁律:绝不明文存储。Vapor 提供了工业级的 BCrypt 算法来对密码做哈希。哈希是单向的:明文可以生成哈希值,但从哈希值几乎不可能反推出明文。BCrypt 在哈希时会自动加盐,让“撞库”复原密码代价大到不可接受;同时它还提供验证明文与哈希值是否匹配的功能。所以创建用户前,要先对明文密码做哈希再存储。加密相关能力都放在 Crypto 库中。
import Crypto
struct UsersController: RouteCollection {
...
func createHandler(_ req: Request, user: User) throws -> Future<User> {
user.password = try BCrypt.hash(user.password)
return user.save(on: req)
}
...
}
这样 user 的密码是以哈希值落库的,即便数据库被拖库,攻击者也很难还原出明文密码。
保证用户名唯一
现在对同名用户没有限制,但后续登录要靠“用户名 + 密码”校验,如果存在重名,登录时就不知道该验证哪一个用户了。因此要给用户名加上唯一约束。
extension User: Migration {
static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
return Database.create(self, on: conn, closure: { (builder) in
try addProperties(to: builder)
builder.unique(on: \.username)
})
}
}
这类似于添加外键约束:如果同名用户已经存在,新的同名用户就会创建失败。
因为改了表字段和约束,需要重置一次数据库让改动生效。在 Xcode 中按住
Option + Run 打开 Schema 设置,给运行时参数加上 revert --all --yes 并勾选运行一次,等价于执行 vapor run revert --all --yes。重置后再运行工程时,记得把该参数取消勾选,新的数据库表关系才会生效。返回数据脱敏
从前面的请求返回里能看到密码的哈希值,这很不安全。解决办法是:返回用户数据前做一次转换,只暴露公开字段。新增一个 Public 嵌套类型,并在 User 上提供转换方法。
final class User: Codable {
...
final class Public: Codable {
var id: UUID?
var name: String
var username: String
init(id: UUID?, name: String, username: String) {
self.id = id
self.name = name
self.username = username
}
}
}
extension User.Public: Content {}
extension User {
func convertToPublic() -> User.Public {
return User.Public(id: self.id, name: self.name, username: self.username)
}
}
extension Future where T: User {
func convertToPublic() -> Future<User.Public> {
return self.map(to: User.Public.self) { (user) -> User.Public in
return user.convertToPublic()
}
}
}
然后让各个处理器都返回 User.Public,查询时也只解码公开字段:
import Vapor
import Crypto
struct UsersController: RouteCollection {
...
func createHandler(_ req: Request, user: User) throws -> Future<User.Public> {
user.password = try BCrypt.hash(user.password)
return user.save(on: req).convertToPublic()
}
func getAllHandler(_ req: Request) throws -> Future<[User.Public]> {
return User.query(on: req).decode(data: User.Public.self).all()
}
func getHandler(_ req: Request) throws -> Future<User.Public> {
return try req.parameters.next(User.self).convertToPublic()
}
func updateHandler(_ req: Request) throws -> Future<User.Public> {
return try flatMap(to: User.Public.self, req.parameters.next(User.self), req.content.decode(User.self)) { (user, updatedUser) -> Future<User.Public> in
user.name = updatedUser.name
user.username = updatedUser.username
return user.save(on: req).convertToPublic()
}
}
...
}
现在所有用户相关的返回数据都不再包含密码相关信息了。