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)
    }
}
中间件顺序至关重要 记住两条规则:(1) ErrorMiddleware 必须放在最外层(最先注册),这样它才能捕获所有内层中间件和路由处理函数抛出的错误;(2) CORSMiddleware 要在认证中间件之前,否则 OPTIONS preflight 请求会因为没有认证 Token 而被拦截,导致跨域请求失败。
本章小结 中间件的洋葱模型让横切关注点(日志、CORS、限流、错误处理)与业务逻辑完全解耦;Request.Storage 是中间件间安全共享数据的正确方式;中间件的注册顺序直接影响行为,ErrorMiddleware 永远最外层。下一章将深入 Swift Concurrency,理解 async/await 与 EventLoopFuture 的区别,以及 Actor 如何保证并发安全。