用户认证:密码哈希与 Token 封面
Vapor 教程

用户认证:密码哈希与 Token

一旦你的应用有了「用户」,就得回答两个问题:你是谁?你能做什么?前者叫认证(Authentication),后者叫授权(Authorization)。这篇先搞定最要命的「密码怎么存才安全」。

一、认证 ≠ 授权

先把认证做对,授权才有意义。而认证的第一铁律是:密码绝不能以明文存进数据库

二、永远不要明文存密码

如果数据库泄露,明文密码就直接曝光了。正确做法是哈希:用 BCrypt 这类算法把密码变成一串不可逆的密文,而且它会自动「加盐」(每人的盐不同,相同密码也会得到不同哈希)。

Vapor 自带 Crypto 模块,一行就搞定:

import Crypto

func createHandler(_ req: Request) throws -> EventLoopFuture<User> {
    let data = try req.content.decode(CreateUserData.self)
    let user = User(
        username: data.username,
        password: try Bcrypt.hash(data.password)
    )
    return user.save(on: req.db).map { user }
}

登录时,用 Bcrypt.verify(明文, against: 哈希) 比对,而不是把哈希「反解」出来——它本来就解不开。

三、User 模型与唯一约束

登录要靠「用户名 + 密码」唯一定位用户,所以用户名不能重复。在迁移里加唯一约束:

struct CreateUser: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        database.schema("users")
            .id()
            .field("username", .string, .required)
            .field("password", .string, .required)
            .unique(on: "username")
            .create()
    }
    func revert(on database: Database) -> EventLoopFuture<Void> {
        database.schema("users").delete()
    }
}
改了表结构要重置:新增字段或约束后,需要重置一次数据库(或写新的迁移)让变动生效。在 Xcode 的 Scheme 运行参数里加上相应 flag 即可,否则会一直报「字段不存在」。

四、基本认证(Basic Auth)

最朴素的方案:客户端把 用户名:密码 用 Base64 塞进请求头 Authorization,服务端解出后查库、验密。

Authorization: Basic base64(用户名:密码)

思路简单,但每次请求都带明文密码(即便 Base64 不是加密),通常只适合内部工具或配合 HTTPS。Vapor 提供了 BasicAuthorization 中间件帮你解析这个头。

五、Token 认证(更常见)

现代 API 更爱 Token:用户登录一次,服务端验证密码正确后发一个令牌(token),之后每次请求带上这个 token 即可,不用反复传密码。

// 登录:验密通过后签发 token
func loginHandler(_ req: Request) throws -> EventLoopFuture<Token> {
    let data = try req.content.decode(LoginData.self)
    return User.query(on: req.db)
        .filter(\.$username == data.username)
        .first()
        .flatMap { user in
            guard let user = user,
                  try Bcrypt.verify(data.password, against: user.password)
            else { throw Abort(.unauthorized) }
            let token = Token(value: [UInt8].random(count: 16).base64, userID: user.id!)
            return token.save(on: req.db).map { token }
        }
}

后续请求在 Authorization: Bearer <token> 里带上令牌,服务端用 Token 中间件校验有效性。这样密码只在登录那一瞬间出现,日常调用更安全。


本系列其他文章