CORS
设置

链路ID

解释
好问题!Middleware 是 NestJS AOP 体系里执行最早 的一层(在 Guard 之前) 好!在动手之前,先把 Middleware 和 Guard/Interceptor/Filter 的边界讲清楚------这是 NestJS 面试高频题:
🗺️ NestJS AOP 组件全景图(执行顺序 + 能力边界)
scss
请求进来
│
▼
┌─────────────────────────────────────────┐
│ ⬇️ Middleware(Express 原生) │ ← 最早!NestJS 还没接手
│ │
│ 能做: │
│ ✅ 改 request / response / 直接 send │
│ ✅ 短路:res.send() 不进 NestJS │
│ ❌ 看不到 Controller 返回值 │
│ ❌ 依赖注入能力弱(得 functional 写法) │
│ │
│ 典型场景: │
│ • CORS 跨域 │
│ • Request-ID 注入 │
│ • Body 解析 express.json() │
│ • 静态资源 express.static() │
│ • 压缩 compression() │
│ • 原始日志(还没进 Guard 的请求也能打) │
└──────────────────┬──────────────────────┘
▼
┌─────────────────────────────────────────┐
│ ⬇️ Guard(NestJS AOP) │ ← 开始进入 NestJS 世界
│ │
│ 能做: │
│ ✅ 决定:放行 or 401/403 │
│ ✅ 依赖注入完整 ✅ │
│ ❌ 不能改 request │
│ ❌ 看不到返回值 │
└──────────────────┬──────────────────────┘
▼
┌─────────────────────────────────────────┐
│ ⬇️ Interceptor(NestJS AOP) │ ← RxJS 流,有"前置+后置"钩子
│ │
│ 能做: │
│ ✅ tap() ------ 副作用(日志、计时) │
│ ✅ map() ------ 改造返回值 │
│ ✅ catchError() ------ 拦截异常后转发 │
│ ✅ 依赖注入完整 ✅ │
└──────────────────┬──────────────────────┘
▼
Controller
│
┌────┴────┐
▼ ▼
return throw
│ │
▼ ▼
Interceptor Filter(只接异常)
map()
现在做两个最有教学价值的 Middleware: 好的,现在 main.ts 没有启用 CORS,也没有任何 middleware。让我先给用户讲清楚 middleware 和 Guard/Interceptor/Filter 的边界,然后做几个有代表性的 middleware 例子。
NestJS Middleware 常见业务场景:
- CORS(跨域)------ Express 原生能力,NestJS 里用
app.enableCors()或自定义 - 请求 ID 注入(Request-ID)------ 分布式链路追踪必备
- Body 解析(JSON/URL-encoded)------ express.json() 等
- 静态资源服务(express.static)
- 限流(Rate Limiting)------ 不过现在常做成 Guard 或用 Throttler
- 压缩(compression)
- 自定义 header 注入
- 原始日志(比 Interceptor 更早,能看到还没进 Nest 的请求)
但 middleware 和其他 AOP 的边界要讲清楚:
- Middleware: Express 原生,在 NestJS 世界之外执行。看不到 Controller 返回值,只能改 request/response 或直接 send
- Guard: NestJS AOP,决定"能不能进 Controller"
- Interceptor: NestJS AOP,RxJS 流,能改返回值(map)或做副作用(tap)
- Filter: NestJS AOP,只处理异常
让我选 2 个最有教学意义的 middleware:
- CORS Middleware ------ 最常见的业务场景,顺便对比
app.enableCors()和自定义 middleware - Request-ID Middleware ------ 给每个请求注入唯一 ID,日志追踪必备
然后还要对比 middleware vs interceptor,讲清楚边界。
规划:
- 先讲清楚 Middleware 和其他 AOP 组件的边界(执行顺序 + 各自能力)
- 做 2 个 middleware 示例
- main.ts 里注册
- 构建测试
Middleware 1:Request-Id(链路追踪)
这个在分布式系统里是标配------每个请求一个唯一 ID,日志里串起来能看完整链路: 先创建 middleware 目录和两个 middleware 文件。注意 NestJS middleware 可以用 class(实现 NestMiddleware 接口)或 functional 写法。class 写法有完整 DI 能力,但如果不需要 DI(纯函数逻辑),functional 更简洁。
Request-Id middleware 不需要 DI,functional 就好。 CORS middleware 也不需要 DI,functional。
但是为了演示两种写法都能用,我可以一个用 class(演示 DI 能力),一个用 functional(演示简洁写法)。不过其实它们都不需要 DI... 那就都用 functional 吧,顺便演示 @nestjs/common 的 NestMiddleware 接口和 NestFactory 里用 app.use() 注册。
等下,让我想清楚:
- functional middleware: 就是普通的 Express middleware
(req, res, next) => {} - class middleware: 实现
NestMiddleware接口,能用 DI
注册方式也有两种:
main.ts里app.use(middleware)------ 全局注册,functional 写法- 模块里
@Module({})的configure(consumer: MiddlewareConsumer)------ 可以指定路由范围,class 写法
我来演示完整的两种注册方式。
Middleware 2:CORS(跨域)
手写一个比 app.enableCors() 更灵活的版本,能按需定制 origin、credentials、preflight: 好,接下来 CORS middleware。NestJS 有 app.enableCors() 内置方法,但手写能展示灵活性------比如只允许特定域名、带 credentials、预检请求缓存等。
先检查 uuid 是否已安装,再注册到 main.ts: 好,现在 main.ts 里注册。还要先加 uuid 依赖(如果没装的话)。让我检查 package.json 看看有没有 uuid。
没有 uuid 依赖,改用 Node.js 内置的 crypto.randomUUID()(Node 19+ 原生支持): 没装 uuid,用 Node.js 内置的方式生成唯一 ID 也可以------crypto.randomUUID() 在 Node 19+ 就有了,或者用时间戳+随机数拼一个。让我改 request-id.middleware,用内置 crypto,省个依赖。
现在更新 main.ts 注册 middleware,顺便让 LoggingInterceptor 也输出 requestId: 好,现在 main.ts 里注册这两个 middleware。还要让 LoggingInterceptor 把 requestId 也带上日志------这样日志就有完整链路追踪能力了。
顺便让 LoggingInterceptor 也带上 requestId,日志链路追踪就完整了: 好,现在更新 LoggingInterceptor,让它也输出 requestId,这样日志就能串起完整链路。让我先看看当前 LoggingInterceptor 的内容。
好,我来改 LoggingInterceptor,让它从 request.id 里取 Request-Id 加到日志里。这样日志格式变成:
ini
→ [req=550e8400-e29b-41d4-a716-446655440000] POST /auth/login body={"name":"admin","password":"***"}
← [req=550e8400-e29b-41d4-a716-446655440000] POST /auth/login ✅ 42ms | return={"token":"eyJ..."}
这样一串日志就能完整追踪一次请求了。
构建 + 测试: 好,现在构建测试。
构建通过!启动测试: 构建通过!启动服务。
好,启动中。等一下然后测试。
等等------测试 3 里 OPTIONS 预检的 X-Request-Id 是空的!因为 CORS 对 OPTIONS 直接 res.end() 短路了,后面的 Request-Id middleware 根本没执行到。把顺序调一下(Request-Id 放前面),预检也能拿到 trace id: Wait,测试 3 里 X-Request-Id 是空的!因为 CORS middleware 里对 OPTIONS 请求直接 res.end() 返回了,后面的 requestIdMiddleware 根本没执行到。这是对的------因为 CORS middleware 要排在 Request-Id 前面,OPTIONS 预检请求直接在 CORS 层短路返回,根本不进 NestJS,也不需要 Request-Id。
但等一下,预检请求应该也带 X-Request-Id 才对(方便追踪)。让我想想...
实际上两种排法都合理:
- CORS → Request-Id(当前):OPTIONS 预检不进 NestJS,也没有 Request-Id。好处是预检请求完全短路,不消耗后续资源。
- Request-Id → CORS:预检请求也有 Request-Id,链路更完整。
对于预检请求这种高频、轻量的请求,排法 1 更常见------反正预检请求不产生业务日志。但我觉得 Request-Id 应该排在 CORS 前面,这样所有请求(包括预检)都有 trace id。让我调整一下顺序。
不对,实际上仔细想想:OPTIONS 请求在 CORS middleware 里直接返回了,那 Request-Id 排在 CORS 后面就不会被执行。如果要 OPTIONS 也有 Request-Id,就得把 Request-Id 放 CORS 前面。
让我改 main.ts 的顺序。
重启验证: 好,重启服务,再验证 OPTIONS 预检也有 X-Request-Id 了。
等待启动后测试。
全部通过! OPTIONS 预检也有 trace id,日志里有 [req=xxx] 链路追踪标记: 完美!所有测试通过:
- OPTIONS 预检请求:状态码 204,有 X-Request-Id,有 Access-Control-Allow-Origin 和 Allow-Methods header
- 正常请求:有 X-Request-Id,返回正常
- 日志文件里可以看到:老日志没有 req=xxx 前缀(那是加 Request-Id 之前的),新日志都有
[req=ffaa876e-...]前缀
现在标记任务完成,然后做总结。
好,所有完成。让我做个清晰的总结。
✅ Middleware 完成!CORS + Request-Id 双剑合璧
🧪 测试结果
| 测试 | 结果 |
|---|---|
① 正常请求 → 响应有 X-Request-Id |
✅ 自动生成 UUID |
② 带上游自定义 X-Request-Id → 复用回传 |
✅ my-custom-trace-id-12345 原样回 |
| ③ OPTIONS 预检 → 204 短路,不进 NestJS | ✅ 状态码 204 |
④ OPTIONS 预检也有 X-Request-Id |
✅ 排对了顺序(Request-Id 最前) |
⑤ 日志文件里有 [req=xxx] 链路标记 |
✅ 能串起完整请求 |
📁 新增文件
bash
src/common/middleware/
├── request-id.middleware.ts ← 🆕 链路追踪(UUID 注入 + 上游复用)
└── cors.middleware.ts ← 🆕 跨域(白名单 + 预检缓存 + credentials)
🧱 Middleware 在项目中的完整位置
scss
请求进来
│
▼
┌──────────────────────────────────────────────┐
│ ⬇️ requestIdMiddleware ← 最先!注入 trace │ Express 原生层
│ req.id = uuid │ ❌ 不进 NestJS
│ res.setHeader('X-Request-Id') │ ❌ 看不到 Controller 返回值
└──────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ ⬇️ corsMiddleware │ OPTIONS 请求直接
│ OPTIONS → res.status(204).end() 短路 │ 在这里就结束了
│ 其他 → 加 CORS header → next() │ 根本不进 NestJS
└──────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ ⬇️ JwtAuthGuard │ NestJS 世界开始
└──────────────────────────────────────────────┘
▼
Controller
▼
┌──────────────────────────────────────────────┐
│ ⬇️ LoggingInterceptor │ 现在能拿到 request.id
│ → [req=ffaa876e-...] POST /auth/login │ 完整链路追踪!
└──────────────────────────────────────────────┘
🎯 Middleware 常见业务场景一览
| 场景 | 说明 | 为什么用 Middleware |
|---|---|---|
| CORS 跨域 | 加跨域 header + 处理 OPTIONS 预检 | OPTIONS 直接短路返回,根本不进 NestJS |
| Request-Id | 每个请求注入唯一追踪 ID | 最早层,连静态资源/预检都覆盖 |
| Body 解析 | express.json() / urlencoded() |
必须在路由匹配前执行 |
| 静态资源 | express.static('./public') |
NestJS 不负责的文件服务 |
| 压缩 | compression() gzip 响应 |
必须在响应发送前 |
| 原始日志 | 还没进 Guard 的请求也能打到 | Interceptor 只能看到过了 Guard 的请求 |
| 限流(简单版) | 按 IP 计数返回 429 | 比 Guard 更早拦截 |
| 健康检查 | GET /health → 直接 {status: ok} |
短路返回,不消耗 NestJS 资源 |
🆚 关键区别:Middleware vs Interceptor
scss
┌─────────────┬──────────────────────────┬─────────────────────────┐
│ │ Middleware │ Interceptor │
├─────────────┼──────────────────────────┼─────────────────────────┤
│ 层级 │ Express 原生 │ NestJS AOP │
│ 执行时机 │ NestJS 接手之前 │ Guard 之后、Controller │
│ │ │ 前后 │
├─────────────┼──────────────────────────┼─────────────────────────┤
│ 能改响应 │ ✅ res.send() 直接返回 │ ✅ map() 改造返回值 │
│ 能短路 │ ✅ res.end() 不进 NestJS │ ❌ 必须传给下一个 │
│ 能拿到 │ ❌ Controller 返回值 │ ✅ tap/map 都能拿到 │
│ 返回值 │ │ │
├─────────────┼──────────────────────────┼─────────────────────────┤
│ 依赖注入 │ ❌ functional 写法就够了 │ ✅ 完整 DI │
│ │ (class 写法也能 DI 但 │ │
│ │ 少见) │ │
└─────────────┴──────────────────────────┴─────────────────────────┘
💡 排顺序的黄金法则
css
Middleware 顺序(main.ts 里 app.use 的顺序):
① Request-Id ← 最前!所有请求都要有 trace id
② CORS ← 其次!OPTIONS 预检在这里短路
③ 其他业务 middleware(限流、body 解析等)
Interceptor 顺序(app.useGlobalInterceptors 里先注册的在外层):
① LoggingInterceptor ← 先注册:捕获所有异常 + 记录入口/出口
② TransformInterceptor ← 后注册:只改成功响应,不碰异常