Chapter 05

认证与 JWT

构建完整的用户认证体系——从注册登录到 JWT 签发,从请求验证到路由保护,每一步都类型安全。

认证 vs 授权:概念区分

在安全领域,认证(Authentication)授权(Authorization)是两个经常被混淆的概念,理解区别是构建安全系统的前提。

认证 (Authentication)

  • 回答"你是谁?"
  • 验证身份的真实性
  • 典型手段:密码、JWT、OAuth
  • 结果:当前请求者是用户 Alice
  • Vapor 中:Authenticator 中间件

授权 (Authorization)

  • 回答"你能做什么?"
  • 检查操作的权限
  • 典型手段:RBAC、ACL、策略
  • 结果:Alice 可以编辑自己的文章
  • Vapor 中:Guard 中间件或业务逻辑

核心概念词典

Authenticatable
Vapor 认证框架的基础协议,Model 实现此协议后才能被认证中间件识别为"可认证的实体"。通常还会搭配 ModelAuthenticatable(密码认证)或 ModelTokenAuthenticatable(Token 认证)使用。
BearerAuthenticator
从请求头的 Authorization: Bearer <token> 中提取令牌并验证的认证器。实现 AsyncBearerAuthenticator 协议,在 authenticate(bearer:for:) 方法中执行验证逻辑(如解析 JWT、查询数据库)。
JWT (JSON Web Token)
一种紧凑的自包含令牌格式,由三部分组成:Header(算法)、Payload(声明/数据)、Signature(签名)。服务端签发后发给客户端,客户端每次请求携带 JWT,服务端验证签名即可确认身份,无需查询数据库。
JWTPayload
JWT 载荷的 Swift 类型表示,实现 JWTPayload 协议。常用声明:sub(Subject,用户 ID)、exp(Expiration,过期时间)、iat(Issued At,签发时间)。自定义载荷可以添加任意业务字段(如 role、email)。
JWTSigner
JWT 签名算法的配置,Vapor 支持:HS256/HS384/HS512(对称 HMAC,需要密钥),RS256/RS384(非对称 RSA,需要公私钥对),ES256(椭圆曲线,更安全高效)。对称算法简单,非对称算法可以分离签发和验证服务。

完整认证流程

注册/登录流程: Client Vapor Server Database │ │ │ │── POST /register {name, email, pw}→│ │ │ │── hash(password) ──────── │ │ │── INSERT user ──────────→ │ │ │←─ user saved ─────────── │ │←── 201 {user} ─────────────────── │ │ │ │ │ │── POST /login {email, password} ──→│ │ │ │── SELECT user by email ──→│ │ │←─ user row ──────────────│ │ │── verify(password, hash) │ │ │── sign JWT(sub=userID) │ │←── 200 {token: "eyJhbG..."} ────── │ │ 已认证请求流程: │── GET /profile │ │ │ Authorization: Bearer eyJhbG... →│ │ │ │── JWTAuthenticator │ │ │ verify(token signature) │ │ │ decode payload │ │ │ req.auth.login(user) │ │ │── handler logic │ │←── 200 {profile} ─────────────────│ │

实现 JWT 认证系统

1. 定义 JWT Payload

// Sources/App/Models/UserPayload.swift
import JWT
import Vapor

struct UserPayload: JWTPayload, Authenticatable {
    // 标准声明
    var sub: SubjectClaim       // 用户 ID
    var exp: ExpirationClaim    // 过期时间
    var iat: IssuedAtClaim      // 签发时间

    // 自定义声明
    var email: String
    var role:  String           // "user" | "admin"

    // 验证声明(框架调用)
    func verify(using signer: JWTSigner) throws {
        try exp.verifyNotExpired()
    }
}

extension UserPayload {
    // 便捷构造器
    init(user: User, expiresIn seconds: Int = 86400) throws {
        let now = Date()
        self.sub   = .init(value: user.id!.uuidString)
        self.exp   = .init(value: now.addingTimeInterval(Double(seconds)))
        self.iat   = .init(value: now)
        self.email = user.email
        self.role  = user.role
    }

    // 从 sub 提取用户 UUID
    var userID: UUID {
        get throws {
            guard let uuid = UUID(uuidString: sub.value) else {
                throw Abort(.unauthorized)
            }
            return uuid
        }
    }
}

2. AuthController:注册与登录

// Sources/App/Controllers/AuthController.swift
import Vapor
import JWT
import Fluent

struct AuthController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let auth = routes.grouped("auth")
        auth.post("register", use: register)
        auth.post("login",    use: login)
        auth.post("refresh", use: refresh)
    }

    // POST /auth/register
    func register(req: Request) async throws -> UserResponse {
        let dto = try req.content.decode(CreateUserDTO.self)
        try CreateUserDTO.validate(content: req)  // Vapor Validations

        let existing = try await User.query(on: req.db)
            .filter(\.$email == dto.email).first()
        guard existing == nil else {
            throw Abort(.conflict, reason: "Email already registered")
        }

        let user = User(
            name:         dto.name,
            email:        dto.email.lowercased(),
            passwordHash: try Bcrypt.hash(dto.password)
        )
        try await user.save(on: req.db)
        return UserResponse(from: user)
    }

    // POST /auth/login
    func login(req: Request) async throws -> TokenResponse {
        let dto = try req.content.decode(LoginDTO.self)

        guard let user = try await User.query(on: req.db)
            .filter(\.$email == dto.email.lowercased()).first() else {
            throw Abort(.unauthorized, reason: "Invalid credentials")
        }

        guard try Bcrypt.verify(dto.password, created: user.passwordHash) else {
            throw Abort(.unauthorized, reason: "Invalid credentials")
        }

        // 签发 JWT
        let payload = try UserPayload(user: user)
        let token   = try req.jwt.sign(payload)
        return TokenResponse(token: token, expiresIn: 86400)
    }
}

3. 配置 JWT 密钥

// configure.swift
import JWT

public func configure(_ app: Application) async throws {
    // JWT 密钥从环境变量读取
    guard let jwtSecret = Environment.get("JWT_SECRET") else {
        throw Abort(.internalServerError, reason: "JWT_SECRET environment variable not set")
    }
    app.jwt.signers.use(.hs256(key: jwtSecret))
}

4. 保护路由

// JWT 认证器:验证 Bearer Token
struct JWTAuthenticator: AsyncBearerAuthenticator {
    func authenticate(bearer: BearerAuthorization, for req: Request) async throws {
        let payload = try req.jwt.verify(bearer.token, as: UserPayload.self)
        // 将 payload 存入 request,供后续处理函数使用
        req.auth.login(payload)
    }
}

// routes.swift — 路由保护
func routes(_ app: Application) throws {
    // 公开路由
    try app.routes.grouped("auth").register(collection: AuthController())

    // 需要认证的路由(JWT 验证失败自动返回 401)
    let protected = app.routes
        .grouped(JWTAuthenticator())
        .grouped(UserPayload.guardMiddleware())

    // 在处理函数中获取当前用户
    protected.get("profile") { req async throws -> UserResponse in
        let payload = try req.auth.require(UserPayload.self)
        guard let user = try await User.find(try payload.userID, on: req.db) else {
            throw Abort(.notFound)
        }
        return UserResponse(from: user)
    }
}
JWT Secret 安全警告 JWT 密钥(secret)绝对不能硬编码在源代码中,不能提交到 Git 仓库。必须通过环境变量或密钥管理服务(如 AWS Secrets Manager、HashiCorp Vault)注入。一旦 secret 泄漏,攻击者可以伪造任意用户的 JWT,整个认证体系将完全失效。
本章小结 Vapor 的认证框架通过 Authenticator 协议将认证逻辑与路由处理解耦;JWT 提供了无状态的身份验证方案,适合分布式服务;guardMiddleware 确保未认证请求在到达处理函数之前就被拦截。下一章将学习中间件的洋葱执行模型,了解如何在请求的任意阶段插入横切逻辑。