主题
Middleware、Guard 与 Interceptor
三者都可参与请求处理,但关注点不同:Middleware 处理底层 HTTP;Guard 决定是否允许访问;Interceptor 包裹成功或失败的路由执行过程。
Middleware
Middleware 最先运行,可以访问 req、res 和 next,但不知道最终要调用的 Controller 方法。适合请求 ID、通用访问日志和底层请求预处理;不要在其中实现权限判断或业务规则。
应用场景
- 请求 ID:在请求最开始生成或透传 ID,后续日志、错误响应和下游调用都能关联同一次请求。此时尚不需要知道具体 Controller,因此放在 Middleware 最合适。
- 访问日志:记录方法、路径、来源 IP 和响应状态。它适用于所有 HTTP 请求,且不依赖路由元数据或业务模型。
- 请求头预处理:例如解析反向代理传入的统一请求头,或注入通用上下文。它只处理协议层数据,不承担认证和授权决策。
ts
import { randomUUID } from 'node:crypto'
import type { NextFunction, Request, Response } from 'express'
type RequestWithId = Request & { requestId?: string }
export function requestId(req: RequestWithId, res: Response, next: NextFunction) {
const id = req.header('x-request-id') ?? randomUUID()
req.requestId = id
res.setHeader('x-request-id', id)
next()
}类式 Middleware
需要依赖注入或希望与其他 Nest Provider 保持一致时,使用 @Injectable() 类并实现 NestMiddleware:
ts
import { Injectable, NestMiddleware } from '@nestjs/common'
import { randomUUID } from 'node:crypto'
import type { NextFunction, Request, Response } from 'express'
type RequestWithId = Request & { requestId?: string }
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: RequestWithId, res: Response, next: NextFunction) {
const id = req.header('x-request-id') ?? randomUUID()
req.requestId = id
res.setHeader('x-request-id', id)
next()
}
}类式 Middleware 通过 Module 注册,因此可在构造函数中注入已由该 Module 导入或提供的 Logger、配置服务等 Provider。
视频用 API Key 展示了类式 Middleware 的完整形态;它适合演示 HTTP 入口校验,但不应替代基于用户、角色和路由元数据的授权:
ts
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common'
import type { NextFunction, Request, Response } from 'express'
@Injectable()
export class ApiKeyMiddleware implements NestMiddleware {
use(req: Request, _res: Response, next: NextFunction) {
if (req.header('x-api-key') !== process.env.INTERNAL_API_KEY) {
throw new UnauthorizedException('无效的 API Key')
}
next()
}
}注册
函数式 Middleware 不需要实现 NestMiddleware,直接传给 app.use() 或 consumer.apply()。全局注册写在 main.ts:
ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { requestId } from './common/request-id.middleware'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.use(requestId)
await app.listen(process.env.PORT ?? 3000)
}
void bootstrap()按路由或模块注册时,在拥有该路由的 Module 中配置:
ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
import { RequestIdMiddleware } from '../common/request-id.middleware'
@Module({})
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('users')
}
}函数式 Middleware 传入 requestId,类式 Middleware 传入 RequestIdMiddleware。同一个 Middleware 选择一种注册方式;不要同时使用 app.use() 和 consumer.apply(),否则同一请求会执行两次。
Guard
Guard 实现 CanActivate,可以通过 ExecutionContext 读取当前请求和路由元数据。认证 Guard 建立当前用户上下文;授权 Guard 根据角色、资源归属或策略决定是否继续。未认证应返回 401,已认证但无权限应返回 403。
应用场景
- 会话或令牌认证:Guard 在 Controller 调用前执行,身份无效时可直接阻断请求,避免无权限请求进入业务逻辑。
- 角色授权:Guard 能读取
@Roles()等路由元数据,因此比 Middleware 更适合判断当前端点是否允许admin、participant等角色访问。 - 端点级访问策略:例如只有已验证邮箱的用户可以创建资源。策略与端点绑定,且需要统一的拒绝响应时使用 Guard。
涉及资源当前状态、库存或复杂领域规则的判断仍放在 Service;Guard 不应替代业务用例。
ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
@Injectable()
export class SessionGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<{ user?: { id: string } }>()
if (!request.user) throw new UnauthorizedException()
return true
}
}下面是视频中用于说明 ExecutionContext 的最小角色 Guard。真实应用不应信任客户端任意传入的 x-role,应由已验证会话或令牌中的可信身份生成 request.user:
ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
@Injectable()
export class RoleGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<{ headers: { 'x-role'?: string } }>()
if (request.headers['x-role'] !== 'admin') {
throw new UnauthorizedException('无权执行此操作')
}
return true
}
}认证与授权应分开:前者确认身份,后者根据身份执行访问策略。会话解析由认证 Guard 统一完成,业务模块不自行解析 Cookie 或令牌。
注册
按 Controller 注册时,该 Controller 的所有路由都会经过 Guard:
ts
import { Controller, UseGuards } from '@nestjs/common'
@UseGuards(SessionGuard)
@Controller('users')
export class UsersController {}按路由方法注册时,只保护单个端点:
ts
import { Controller, Get, UseGuards } from '@nestjs/common'
@Controller('users')
export class UsersController {
@Get('me')
@UseGuards(SessionGuard)
findMe() {}
}@UseGuards() 可接收多个 Guard,按声明顺序执行。Controller 或路由级注册适合局部访问策略;所有端点默认需要认证时,再使用全局 Guard。
不依赖其他 Provider 的 Guard 可在 main.ts 注册为全局 Guard:
ts
app.useGlobalGuards(new SessionGuard())需要注入认证服务、配置或日志时,使用 APP_GUARD 在 Module 中注册:
ts
import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'
import { SessionGuard } from './session.guard'
@Module({
providers: [
{
provide: APP_GUARD,
useClass: SessionGuard,
},
],
})
export class AuthModule {}Interceptor
Interceptor 在调用前后运行,适合耗时监控、缓存、追踪和成功响应转换。它返回 next.handle() 产生的 Observable;错误仍应交给 Exception Filter,不要在响应包装逻辑中抹平错误状态码。
应用场景
- 耗时与追踪日志:Interceptor 可以在调用前记录开始时间、在返回后计算耗时,因此比只在入口运行的 Middleware 更适合观察完整的 Controller 和 Service 执行时间。
- 统一成功响应:路由处理器返回数据后,Interceptor 能统一附加
data、请求 ID 或分页元数据;错误响应仍由Exception Filter保持独立语义。 - 缓存:Interceptor 可以在调用前命中缓存直接返回,或在调用后缓存结果,适合读多写少且缓存策略跨多个端点复用的场景。
- 序列化与脱敏:在返回客户端前移除密码散列、内部标记等字段,避免每个 Controller 手动处理相同输出规则。
ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { tap } from 'rxjs'
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const startedAt = Date.now()
const request = context.switchToHttp().getRequest<{ method: string; url: string }>()
return next.handle().pipe(
tap(() => {
console.info(`${request.method} ${request.url} ${Date.now() - startedAt}ms`)
}),
)
}
}视频的 TransformInterceptor 将每个成功结果包装为固定结构。next.handle() 执行 Controller 并得到其结果,map() 再转换该结果;它不处理异常。
ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import type { Response } from 'express'
import { map } from 'rxjs'
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T> {
intercept(context: ExecutionContext, next: CallHandler<T>) {
const response = context.switchToHttp().getResponse<Response>()
return next.handle().pipe(
map((data) => ({
statusCode: response.statusCode,
message: 'Success',
data,
})),
)
}
}注册
不依赖其他 Provider 的 Interceptor 可在 main.ts 注册:
ts
app.useGlobalInterceptors(new TimingInterceptor())需要注入 Provider 的全局 Interceptor 使用 APP_INTERCEPTOR 注册:
ts
import { Module } from '@nestjs/common'
import { APP_INTERCEPTOR } from '@nestjs/core'
import { TimingInterceptor } from './timing.interceptor'
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TimingInterceptor,
},
],
})
export class ObservabilityModule {}app.useGlobalInterceptors() 注册的实例不在模块依赖注入上下文中。按 Controller 或路由方法注册时使用 @UseInterceptors(TimingInterceptor)。
