中间件的洋葱执行模型
中间件(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 如何保证并发安全。