Token 认证 封面
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.TokenBearerAuthenticatable,并让 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 都加了认证中间层保护,只有认证通过的用户才能执行相关操作。


本系列其他文章