Chapter 06

中间件

理解洋葱模型的执行顺序,用中间件优雅地处理横切关注点:日志、CORS、限流、错误包装。

中间件的洋葱执行模型

中间件(Middleware)是一种在请求到达处理函数之前、响应返回客户端之前插入逻辑的机制。Vapor 的中间件采用洋葱模型:请求由外向内穿过每一层中间件,响应由内向外经过同样的层。

这种双向穿透的特性使得同一个中间件可以同时处理请求阶段(前置逻辑)和响应阶段(后置逻辑),非常适合实现日志记录(记录请求和响应)、计时(测量处理耗时)、错误处理(捕获异常包装成统一格式)等场景。

洋葱模型执行顺序(注册顺序:ErrorMiddleware → CORS → Log → Route): 请求 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→ │ ┌─────────────────────────────────────────────────┼──────┐ │ ErrorMiddleware ↓ 进入 ↑ 返回 │ │ │ ┌────────────────────┼──────────────────┼────┐ │ │ │ │ CORSMiddleware ↓ ↑ │ │ │ │ │ ┌─────────────────┼──────────────────┼─┐ │ │ │ │ │ │ LogMiddleware ↓ ↑ │ │ │ │ │ │ │ ┌──────────────┼──────────────────┼┐│ │ │ │ │ │ │ │ RouteHandler ↓ 业务逻辑 ↑ ││ │ │ │ │ │ │ │ └──────────────────────────────────┘│ │ │ │ │ │ └───────────────────────────────────────┘ │ │ │ │ └─────────────────────────────────────────────┘ │ │ └───────────────────────────────────────────────────┘ │ │ 响应 ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←← 关键点:中间件注册顺序决定"洋葱层"的先后, ErrorMiddleware 放最外层才能捕获所有内层的错误。

核心概念词典

Middleware / AsyncMiddleware
Vapor 中间件协议,核心方法是 respond(to request:chainingTo next:)。调用 next.respond(to: request) 将请求传递给下一层,可在此之前修改请求,在此之后处理响应。AsyncMiddleware 是 Swift Concurrency 版本,推荐使用。
Request.Storage
请求级别的键值存储,用于在中间件与处理函数之间、以及不同中间件之间共享数据。通过 StorageKey 枚举键访问,类型安全,生命周期与请求绑定,不会跨请求泄漏数据。
CORSMiddleware
Vapor 内置的跨域资源共享中间件,处理浏览器发出的 CORS preflight 请求(OPTIONS 方法),并在所有响应中添加相应的 Access-Control-* 头部。通过 CORSMiddleware.Configuration 配置允许的来源、方法和头部。
ErrorMiddleware
Vapor 默认注册的错误处理中间件,捕获所有从路由处理函数中抛出的 Error,将其转换为 HTTP 错误响应。AbortError 类型会使用其指定的状态码;其他 Error 类型默认转换为 500。

自定义中间件:请求日志

一个完整的请求日志中间件需要记录:请求方法、路径、来源 IP、处理耗时、响应状态码。以下实现利用了洋葱模型的双向特性,在请求阶段记录开始时间,在响应阶段计算耗时:

// Sources/App/Middleware/RequestLogMiddleware.swift
import Vapor
import Foundation

struct RequestLogMiddleware: AsyncMiddleware {

    func respond(to request: Request,
                 chainingTo next: AsyncResponder) async throws -> Response {

        // ── 请求阶段(进入)────────────────────────
        let startTime = Date()
        let requestID = UUID().uuidString.prefix(8)
        let ip        = request.headers["X-Forwarded-For"].first
                        ?? request.remoteAddress?.hostname
                        ?? "unknown"

        request.logger.info("[\(requestID)] → \(request.method) \(request.url.path) from \(ip)")

        // 存入 Request.Storage 供下游使用
        request.storage[RequestIDKey.self] = String(requestID)

        do {
            // ── 传递给下一层 ──────────────────────────
            let response = try await next.respond(to: request)

            // ── 响应阶段(返回)────────────────────────
            let elapsed = Int(Date().timeIntervalSince(startTime) * 1000)
            request.logger.info("[\(requestID)] ← \(response.status.code) (\(elapsed)ms)")

            // 在响应头中附加 Request-ID
            response.headers.replaceOrAdd(name: "X-Request-ID", value: String(requestID))
            return response

        } catch {
            let elapsed = Int(Date().timeIntervalSince(startTime) * 1000)
            request.logger.error("[\(requestID)] ✗ Error: \(error) (\(elapsed)ms)")
            throw error  // 继续向外抛,让 ErrorMiddleware 处理
        }
    }
}

// Request.Storage Key
private enum RequestIDKey: StorageKey {
    typealias Value = String
}

// 扩展 Request,方便访问
extension Request {
    var requestID: String? {
        storage[RequestIDKey.self]
    }
}

CORS 配置

前后端分离项目中,浏览器的同源策略会阻止跨域请求。CORSMiddleware 负责处理浏览器发出的 OPTIONS preflight 请求,并在响应中附加允许跨域的头部:

// configure.swift — CORS 配置
let corsConfig = CORSMiddleware.Configuration(
    allowedOrigin:  .any([
        "https://myapp.com",
        "https://www.myapp.com",
        "http://localhost:3000"   // 开发环境
    ]),
    allowedMethods: [.GET, .POST, .PUT, .PATCH, .DELETE, .OPTIONS],
    allowedHeaders: [.accept, .authorization, .contentType, .origin],
    allowCredentials: true,
    cacheExpiration: 600   // preflight 缓存 600 秒
)

// 注册顺序很关键!CORS 必须在路由之前,ErrorMiddleware 之后
app.middleware.use(ErrorMiddleware.default(environment: app.environment))
app.middleware.use(CORSMiddleware(configuration: corsConfig))
app.middleware.use(RequestLogMiddleware())
app.middleware.use(RateLimitMiddleware(requestsPerMinute: 60))

限流中间件

限流(Rate Limiting)防止 API 被滥用或遭受暴力破解。以下是基于内存的简单令牌桶实现,生产环境建议使用 Redis 实现分布式限流:

actor RateLimiter {
    private var buckets: [String: (Int, Date)] = [:]
    private let limit:   Int
    private let window:  TimeInterval = 60  // 60 秒窗口

    init(requestsPerMinute: Int) { limit = requestsPerMinute }

    func check(ip: String) -> Bool {
        let now = Date()
        if let (count, windowStart) = buckets[ip],
           now.timeIntervalSince(windowStart) < window {
            if count >= limit { return false }
            buckets[ip] = (count + 1, windowStart)
        } else {
            buckets[ip] = (1, now)
        }
        return true
    }
}

struct RateLimitMiddleware: AsyncMiddleware {
    private let limiter: RateLimiter

    init(requestsPerMinute: Int) {
        limiter = RateLimiter(requestsPerMinute: requestsPerMinute)
    }

    func respond(to request: Request,
                 chainingTo next: AsyncResponder) async throws -> Response {
        let ip = request.remoteAddress?.hostname ?? "unknown"
        guard await limiter.check(ip: ip) else {
            throw Abort(.tooManyRequests, reason: "Rate limit exceeded")
        }
        return try await next.respond(to: request)
    }
}
中间件顺序错误是最常见的 Bug 来源 记住两条规则:(1) ErrorMiddleware 必须放在最外层(最先注册),这样它才能捕获所有内层中间件和路由处理函数抛出的错误;(2) CORSMiddleware 要在认证中间件之前,否则 OPTIONS preflight 请求会因为没有认证 Token 而被拦截,导致跨域请求失败。错误的顺序会导致难以调试的问题,因为行为取决于洋葱层的组合。

自定义错误中间件

Vapor 默认的 ErrorMiddleware 将所有错误格式化为 {"error": true, "reason": "..."}。生产环境通常需要更丰富的错误响应格式,包含错误码、调试信息(仅开发环境)和 Request ID 等:

// Sources/App/Middleware/CustomErrorMiddleware.swift
import Vapor

struct CustomErrorMiddleware: AsyncMiddleware {

    // 统一错误响应体格式
    struct ErrorResponse: Content {
        let code:      Int         // HTTP 状态码
        let message:   String      // 错误描述
        let requestID: String?    // 来自 Request.Storage 的追踪 ID
        let details:   String?    // 仅开发环境输出
    }

    let environment: Environment

    func respond(to request: Request,
                 chainingTo next: AsyncResponder) async throws -> Response {
        do {
            return try await next.respond(to: request)
        } catch {
            return self.errorResponse(for: error, request: request)
        }
    }

    private func errorResponse(for error: Error, request: Request) -> Response {
        let status: HTTPResponseStatus
        let message: String

        if let abort = error as? AbortError {
            status  = abort.status
            message = abort.reason
        } else {
            status  = .internalServerError
            message = "An internal error occurred"
            request.logger.error("Unhandled error: \(error)")
        }

        let body = ErrorResponse(
            code:      Int(status.code),
            message:   message,
            requestID: request.requestID,   // 来自 RequestLogMiddleware
            details:   environment.isRelease() ? nil : "\(error)"
        )

        let response = Response(status: status)
        try? response.content.encode(body)
        return response
    }
}
在中间件中访问 Request.Storage 的注意事项 Request.Storage 的数据只有在相应中间件已经执行后才能读取。例如,RequestLogMiddleware 将 requestID 写入 Storage,那么只有在它的下游(内层)才能读到该值。如果 CustomErrorMiddleware 注册在 RequestLogMiddleware 之外(更早注册),则其 catch 块执行时 requestID 可能尚未写入。正确做法:把 RequestLogMiddleware 放在 CustomErrorMiddleware 的外层(更早注册)。

路由级别中间件

并非所有中间件都需要全局注册。有些中间件只对特定路由组生效,比如管理员权限检查只需要在 /admin 路由组上:

// configure.swift — 全局中间件(对所有路由生效)
app.middleware.use(CustomErrorMiddleware(environment: app.environment))
app.middleware.use(CORSMiddleware(configuration: corsConfig))
app.middleware.use(RequestLogMiddleware())

// routes.swift — 路由级别中间件(只对特定路由组生效)
func routes(_ app: Application) throws {

    // 公开 API:全局中间件已处理日志/CORS/错误
    let publicAPI = app.routes.grouped("api", "v1")
    publicAPI.post("register", use: AuthController().register)

    // 需要认证的 API:在路由组上追加认证中间件
    let authenticated = publicAPI
        .grouped(UserAuthenticator())        // 解析 JWT
        .grouped(User.guardMiddleware())     // 确保用户已认证
    try authenticated.register(collection: UserController())

    // 管理员路由:额外叠加管理员权限检查
    let admin = authenticated
        .grouped("admin")
        .grouped(AdminCheckMiddleware())    // 检查用户角色是否为 admin
    try admin.register(collection: AdminController())
}

// AdminCheckMiddleware 示例
struct AdminCheckMiddleware: AsyncMiddleware {
    func respond(to request: Request,
                 chainingTo next: AsyncResponder) async throws -> Response {
        let user = try request.auth.require(User.self)
        guard user.role == .admin else {
            throw Abort(.forbidden, reason: "Admin access required")
        }
        return try await next.respond(to: request)
    }
}
本章小结 中间件的洋葱模型让横切关注点(日志、CORS、限流、错误处理)与业务逻辑完全解耦。关键原则:(1) ErrorMiddleware 永远最外层,捕获所有内层抛出的错误;(2) CORSMiddleware 在认证中间件之前,避免 preflight 被认证拦截;(3) Request.Storage 是中间件间类型安全共享数据的正确方式;(4) 路由级别中间件通过 grouped() 叠加,比全局注册更精细。自定义 ErrorMiddleware 是生产环境标准化错误响应格式的最佳位置。