中间件的洋葱执行模型
中间件(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 是生产环境标准化错误响应格式的最佳位置。