Vapor 大型教程
Token 认证
Basic 认证后只有登录用户能创建缩略语,但其它请求每次都传密码太麻烦。本文让用户登录一次换取 token,之后请求只带 token 即可。
为什么需要 token
Basic 认证实现后,只有被认证通过的用户才能创建缩略语,但其它请求并没有被保护起来;更关键的是,如果每次请求都要传“用户名:密码”的认证头,会非常麻烦也不安全。我们可以让用户登录一次,认证通过后发给他一个“已登录凭证”(token)由他自己保存,之后的请求都带上这个 token 即可。注意:token 不能存用户名密码明文。
创建 Token 模型
新建一个 Token 模型,保存随机生成的 token 字符串,以及它所属的用户 ID:
import Foundation
import Vapor
import FluentPostgreSQL
import Authentication
final class Token: Codable {
var id: UUID?
var token: String
var userID: User.ID
init(token: String, userID: User.ID) {
self.token = token
self.userID = userID
}
}
extension Token: PostgreSQLUUIDModel {}
extension Token: Migration {
static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
return Database.create(self, on: conn) { builder in
try addProperties(to: builder)
builder.reference(from: \.userID, to: \User.id)
}
}
}
extension Token: Content {}
extension Token {
static func generate(for user: User) throws -> Token {
let random = try CryptoRandom().generateData(count: 16)
return try Token(token: random.base64EncodedString(), userID: user.requireID())
}
}
注册 Token 迁移
别忘了把 Token 加入迁移配置:
migrations.add(model: Token.self, database: .psql)
登录接口
在 UsersController 里加一个用 Basic 认证保护的 login 路由。登录成功后,为当前用户生成一个 token 并保存:
struct UsersController: RouteCollection {
func boot(router: Router) throws {
...
let basicAuthMiddleware = User.basicAuthMiddleware(using: BCryptDigest())
let basicAuthGroup = usersGroup.grouped(basicAuthMiddleware)
basicAuthGroup.post("login", use: login)
}
func login(_ req: Request) throws -> Future<Token> {
let user = try req.requireAuthenticated(User.self)
let token = try Token.generate(for: user)
return token.save(on: req)
}
}
让 Token 遵循认证协议
为了让 Vapor 的认证中间件能识别这个 token,需要让 Token 遵循 Authentication.Token 与 BearerAuthenticatable,并让 User 遵循 TokenAuthenticatable:
extension Token: Authentication.Token {
typealias UserType = User
static let userIDKey: UserIDKey = \Token.userID
}
extension Token: BearerAuthenticatable {
static let tokenKey: TokenKey = \Token.token
}
extension User: TokenAuthenticatable {
typealias TokenType = Token
}
用 token 中间件保护路由
把创建缩略语等写操作改用 token 中间件保护。认证通过后可在处理器里通过 req.requireAuthenticated(User.self) 拿到当前登录用户,把它记到 Acronym 上:
struct AcronymCreateData: Content {
let short: String
let long: String
}
struct AcronymsController: RouteCollection {
func boot(router: Router) throws {
let routeGroup = router.grouped("api", "acronyms")
let tokenAuthMiddleware = User.tokenAuthMiddleware()
let guardAuthMiddleware = User.guardAuthMiddleware()
let protectd = routeGroup.grouped(tokenAuthMiddleware, guardAuthMiddleware)
protectd.post(AcronymCreateData.self, use: createHandler)
...
}
func createHandler(_ req: Request, data: AcronymCreateData) throws -> Future<Acronym> {
let user = try req.requireAuthenticated(User.self)
let acronym = try Acronym(short: data.short, long: data.long, userID: user.requireID())
return acronym.save(on: req)
}
...
}
除创建接口外,其它可能破坏数据的接口(更新、删除、关联分类、创建分类、创建用户等)也要加上 token 中间件保护。对 GET 请求一般不加保护,目的是让未登录用户也有读取权限。
给其它写操作加保护
更新缩略语时同样记录当前用户:
protectd.put(Acronym.parameter, use: updateHandler)
protectd.delete(Acronym.parameter, use: deleteHandler)
protectd.post(Acronym.parameter,"categories", Category.parameter, use: addCategoriesHandler)
protectd.delete(Acronym.parameter, "categories", Category.parameter, use: removeCategoriesHandler)
...
func updateHandler(_ req: Request) throws -> Future<Acronym> {
return try flatMap(to: Acronym.self, req.parameters.next(Acronym.self), req.content.decode(AcronymCreateData.self)) { (acronym, updateData) -> Future<Acronym> in
acronym.short = updateData.short
acronym.long = updateData.long
let user = try req.requireAuthenticated(User.self)
acronym.userID = try user.requireID()
return acronym.save(on: req)
}
}
分类控制器与用户控制器的创建接口也用同样的方式保护:
struct CategoriesController: RouteCollection {
func boot(router: Router) throws {
...
let tokenAuthMiddleware = User.tokenAuthMiddleware()
let guardAuthMiddleware = User.guardAuthMiddleware()
let tokenAuthGroup = categoriesRoute.grouped(tokenAuthMiddleware, guardAuthMiddleware)
tokenAuthGroup.post(Category.self, use: createHandler)
}
...
let tokenAuthMiddleware = User.tokenAuthMiddleware()
let guardAuthMiddleware = User.guardAuthMiddleware()
let tokenAuthGroup = usersGroup.grouped(tokenAuthMiddleware, guardAuthMiddleware)
tokenAuthGroup.post(User.self, use: createHandler)
现在所有可能破坏数据的 API 都加了认证中间层保护,只有认证通过的用户才能执行相关操作。