Middleware / Guard / Interceptor的区别
让我用一张流程图先把 Middleware / Guard / Interceptor 的执行位置 搞清楚,再回答你。
🗺️ 完整请求生命周期:谁先谁后
bash
HTTP 请求进来
│
▼
┌─────────────────────────────────────────────────┐
│ ① Middleware(最早) │ ← Express 原生层
│ - 最早拿到 req/res 对象 │
│ - 可以修改 req,也可以直接结束响应 │
│ - 不知道后面是哪个 Controller 方法 │
└────────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ ② Guard(鉴权) │ ← NestJS 层
│ - 决定"能不能进 Controller" │
│ - 能看到:Controller 类、方法、装饰器 │
│ - 返回 true=放行,false=403 │
└────────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ ③ Interceptor 前置段 │
└────────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ ④ Pipe(参数校验) │
└────────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ ⑤ Controller 执行业务逻辑 │
└────────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ ⑥ Interceptor 后置段 │ ← 能看到返回值 + 耗时
└────────────────────┬────────────────────────────┘
▼
HTTP 响应出去
🔑 问题 1:登录鉴权用 Guard 还是 Middleware?
答:99% 用 Guard,Middleware 极少用于鉴权。 原因:
| Middleware | Guard | |
|---|---|---|
| 知道"要进哪个 Controller"吗 | ❌ 完全不知道 | ✅ 知道 Controller 类 + 方法 |
| 能读装饰器吗 | ❌ 不能 | ✅ 能读 @Roles('admin') 等装饰器 |
| 能区分"哪些接口要登录"吗 | ❌ 自己硬编码 URL 路径 | ✅ 靠装饰器灵活标记 |
| 能返回 NestJS 格式的错误吗 | ❌ 要手动拼 HTTP | ✅ throw new UnauthorizedException() 自动 401 |
对比代码就懂了:
ts
// ❌ 用 Middleware 鉴权 ------ 很痛苦
@Injectable()
export class AuthMiddleware implements NestMiddleware {
use(req, res, next) {
// 我怎么知道哪个路径要登录?只能硬编码路径!
if (req.url.startsWith('/admin') || req.url.startsWith('/user')) {
const token = req.headers.authorization;
if (!token) {
// 还要自己手动返回 JSON
res.status(401).json({ code: 401, msg: '未登录', data: null });
return;
}
req.user = verify(token);
}
next();
}
}
// ✅ 用 Guard 鉴权 ------ 很舒服
@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) throw new UnauthorizedException('请先登录');
request.user = jwtService.verify(token);
return true;
}
}
// Controller 里用装饰器灵活标记
@Controller('user')
@UseGuards(JwtAuthGuard) // 整个 Controller 生效
export class UserController {
@Get()
findAll() { ... }
@Public() // 这个接口例外,不需要登录
@Get('public-info')
getPublicInfo() { ... }
}
Middleware 做鉴权会变成 URL 路径硬编码地狱------每加一个要跳过的接口就要改中间件里的 if-else。
🔑 问题 2:日志放 Middleware 还是 Interceptor?
答:两者都能做日志,但 Interceptor 更好。 关键区别:
| 能力 | Middleware | Interceptor |
|---|---|---|
| 能看到请求进来(方法、URL、参数) | ✅ | ✅ |
| 能看到响应出去(返回值) | ❌ | ✅ |
| 能算耗时 | ❌ 只能在结束时大概估 | ✅ 精准(前后都有钩子) |
| 能区分成功/失败 | ❌ | ✅ tap/catchError |
| 能看到 Controller 方法名 | ❌ | ✅ |
能区分 throw 异常 |
❌ 只能看到响应状态码 | ✅ catchError 拿到完整 error 对象 |
Middleware 做日志的困境
ts
@Injectable()
export class LogMiddleware implements NestMiddleware {
use(req, res, next) {
const start = Date.now();
console.log(`→ ${req.method} ${req.url}`);
// Middleware 怎么拿响应数据?
// 方法 1:监听 res 'finish' 事件 ------ 只能拿到状态码,拿不到 body
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`← ${req.method} ${req.url} ${res.statusCode} ${duration}ms`);
// ❌ 拿不到返回的 data!只能看到状态码
});
// 方法 2:猴子补丁 res.write/res.end ------ 改写 Express 内部,恶心
// 方法 3:拦截 JSON.parse ------ 复杂且脆弱
next();
}
}
Middleware 的问题 :Express 的 res 对象在 finish 之后已经把 body 发出去了,你拿不到内容。要想拿到只能改 Express 内部的方法------非常 hacky。
Interceptor 天然能拿到完整信息
ts
return next.handle().pipe(
tap((data) => {
// ✅ data 就是 Controller 的返回值!想怎么看就怎么看
log(`← ${duration}ms | return=${JSON.stringify(data)}`);
}),
catchError((err) => {
// ✅ err 就是完整的异常对象!能拿到 message、stack
log(`← ${duration}ms | ❌ ${err.message}`);
return throwError(() => err);
}),
);
Interceptor 基于 RxJS Observable,Controller 的返回值/异常都是流里的数据------想怎么处理就怎么处理。
📋 什么时候该用 Middleware?
虽然不适合鉴权和日志,但 Middleware 有它专属的场景:
| Middleware 场景 | 为什么它适合 |
|---|---|
| CORS 跨域 | 需要在最早设置响应头 |
| body-parser / cookie-parser | 需要在参数解析之前介入 |
| 压缩 / 防篡改 | Express 原生能力,NestJS 没替代 |
| 请求 ID 注入 | 在最早生成 traceId 塞到 req 上,后续所有组件都能用 |
| Express 风格的限流 | 基于 Express 原生 req.ip 的简单限流 |
🎯 登录鉴权的正确做法(预览)
sql
POST /auth/login { username, password }
│
▼
AuthService.login() → 查库 + bcrypt.compare → 签发 JWT
│
▼
返回 { token: "eyJhbGciOi..." }
GET /user/profile Headers: Authorization: Bearer eyJ...
│
▼
┌───────────────────────────────────┐
│ JwtAuthGuard.canActivate() │ ← Guard 验 token
│ 解析 payload → 挂到 req.user │
└───────────────┬───────────────────┘
▼
Controller 拿到 user
@CurrentUser() user
需要的东西:
@nestjs/jwt依赖(签发/验证 JWT)bcryptjs依赖(密码加密)AuthService(登录/注册业务逻辑)JwtAuthGuard(验 token)@Public()装饰器(标记免登录接口)@CurrentUser()装饰器(从 token 取用户信息)
📌 一句话总结
Middleware 是 Express 的"前置钩子",适合 CORS、body 解析这种基础设施级的事情;Guard 是 NestJS 的"门禁",适合鉴权(能读装饰器、能区分接口);Interceptor 是"切片",适合日志、格式化这种"既能看前面又能看后面"的事情。 三者各有分工,不要混用------鉴权用 Guard,日志用 Interceptor,Middleware 留给最底层的基础设施。
鉴权
安装依赖
bash
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs
bash
npm install -D @types/bcryptjs @types/passport-jwt
User实体加password字段+UserService加 hash逻辑

创建 auth 模块骨架(module/service/controller/guards/decorators)

测试
没有token请求@UseGuards(JwtAuthGuard)下的非@Public()

没有token请求@UseGuards(JwtAuthGuard)下的@Public()

有token请求@UseGuards(JwtAuthGuard)下的非@Public()

RABC权限

解释
步骤
Step 1:登录时把 role name 也塞进 JWT
Step 2 & 3:创建 @Roles 装饰器 + RolesGuard
Step 4:AuthModule 导出 RolesGuard + 在 Controller 上用
现在在 RoleController 上加上 RBAC(roleId=1 是 admin,只有 admin 能管理角色):
Step 5:构建 + 测试 RBAC 完整流程
两个 Role 创建成功!现在注册两个用户(admin 和 user),分别拿 token 测 RBAC:
两个 token 都拿到了!admin 的 role=admin,alice 的 role=user。现在测试 admin 能写、user 不能写:
RBAC 全部测试通过! 🎉🎊 RBAC 测试完美跑通!总结一下测试结果:
| 测试 | 用户 | 结果 | 说明 |
|---|---|---|---|
| ⑦ admin 删除 Role/2 | admin token | FOREIGN KEY 错误(不是 403!) | admin 通过了 RolesGuard!数据库层才报错,说明 RBAC 放行成功 |
| ⑧ alice 删除 Role/2 | user token | 403 Forbidden | "角色 \"user\" 没有权限访问此接口(需要: admin)" ✅ |
| ⑨ alice 读取 Role | user token | 200 OK | 读接口没 @Roles 限制,RBAC 放行 ✅ |
完美!admin 能进删除接口(数据库报错是因为 Role/2 下还有 alice 用户,这是另一个业务逻辑),user 角色直接 403。
标记完成。
总结一下,用清晰的结构给用户展示 RBAC 的实现要点。
✅ RBAC 角色权限跑通!全流程测试结果
🧪 测试矩阵
| # | 用户 | Token 里的 role | 操作 | 接口 | 结果 |
|---|---|---|---|---|---|
| ⑦ | admin | admin |
删除 Role | DELETE /role/2 |
✅ 通过 RBAC(DB 层因有用户报错,不是 403) |
| ⑧ | alice | user |
删除 Role | DELETE /role/2 |
✅ 403 Forbidden "角色 user 没有权限(需要: admin)" |
| ⑨ | alice | user |
读 Role | GET /role |
✅ 200 OK(读接口没 @Roles 限制) |
📁 新增的 2 个文件
kotlin
src/auth/
├── decorators/
│ ├── public.decorator.ts ← 之前的
│ ├── current-user.decorator.ts ← 之前的
│ └── roles.decorator.ts ← 🆕 @Roles('admin', 'user') 贴元数据
├── guards/
│ ├── jwt-auth.guard.ts ← 之前的
│ └── roles.guard.ts ← 🆕 校验 request.user.role 是否匹配
🔗 Guard 执行链(顺序很重要!)
arduino
请求进来
│
▼
┌─────────────────────┐
│ JwtAuthGuard │ 第 1 层:token 合法吗?
│ (CanActivate) │ ✅ 通过 → request.user = { role: 'admin', ... }
│ │ ❌ 401 "请先登录"
└────────┬────────────┘
▼
┌─────────────────────┐
│ RolesGuard │ 第 2 层:角色匹配吗?
│ (CanActivate) │ ✅ 通过 → 进入 Controller
│ │ ❌ 403 "角色 user 没有权限"
└────────┬────────────┘
▼
Controller
💡 关键设计:role name 直接存在 JWT 里
ts
// auth.service.ts ------ 登录时查 role
const user = await this.userRepository
.createQueryBuilder('u')
.addSelect('u.password')
.leftJoinAndSelect('u.role', 'r') // ← 顺便把 role name 查出来
.where('u.name = :name', { name: dto.name })
.getOne();
const payload = {
sub: user.id,
role: user.role?.name ?? 'unknown', // ← JWT 里存角色名
};
为什么这样设计? RolesGuard 不需要再查 DB!从 token 解出来 request.user.role === 'admin' 直接判断,0 次额外查询。
🎯 使用姿势总览
ts
// 类级 ------ 所有方法都要 admin
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('admin')
export class AdminController { ... }
// 方法级 ------ 只有特定方法要 admin
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('role')
export class RoleController {
@Get() // 所有人(已登录)可读
findAll() { ... }
@Roles('admin') // 只有 admin 能写
@Delete(':id')
remove() { ... }
@Roles('admin', 'editor') // 多个角色也行
@Patch(':id')
update() { ... }
}
总结
| 新东西 | 核心代码 | 作用 |
|---|---|---|
多角色 @Roles('admin', 'editor') |
requiredRoles.includes(user.role) |
一个接口允许多种角色 |
| leftJoinAndSelect | .leftJoinAndSelect('u.role', 'r') |
QueryBuilder 关联查询(比 relations: 灵活,还能配合 addSelect) |
| Guard 顺序 | @UseGuards(JwtAuthGuard, RolesGuard) |
按数组顺序执行,前一个挂的数据后一个能用 |
@CurrentUser() + role 组合做数据权限(admin 能看所有用户,普通用户只能看自己)
解释
Step 1:UserService ------ 数据权限在 Service 层
现在我来重写 UserService,给所有公开方法加数据权限逻辑。让我想想哪些方法需要改:
findAll()------ admin 全部,user 只查自己findOne(id)------ admin 全查,user 只能查自己的 idfindOneWithRole(id)------ 同上update(id)------ admin 全改,user 只能改自己remove(id)------ admin 全删,user 只能删自己findAllWithRole()------ 同 findAll
create() 不用 ------ 注册就是所有人能创建
参数设计:currentUser?: { sub: number; name: string; role: string }
让我写出来。
Step 2:UserController ------ 加 RolesGuard + 透传 @CurrentUser
好,现在更新 UserController,给每个方法加 @CurrentUser() user 参数并传给 Service。注意 create 方法是 @Public() 的,不需要 currentUser。
Step 3:构建 + 测试数据权限
好,现在构建 + 启动 + 测试。数据权限的测试矩阵:
- admin → GET /user → 全部用户(2个)
- alice(user)→ GET /user → 只看到自己(1个)
- alice → GET /user/1(admin 的 id)→ 403 Forbidden
- alice → GET /user/2(自己)→ 200 OK
- admin → GET /user/2 → 200 OK
- alice → PATCH /user/2(改自己)→ 200 OK
- alice → PATCH /user/1(改 admin)→ 403 Forbidden
🧪 完整测试矩阵
| # | 谁 | 操作 | 接口 | 结果 |
|---|---|---|---|---|
| ① | admin | 列表 | GET /user |
✅ 2 个用户全部返回 |
| ② | alice | 列表 | GET /user |
✅ 只返回自己(1 个) |
| ③ | alice | 查别人 | GET /user/1(admin id) |
✅ 403 Forbidden 越权拦截 |
| ④ | alice | 查自己 | GET /user/2 |
✅ 200 OK |
| ⑤ | admin | 查别人 | GET /user/2 |
✅ 200 OK |
| ⑥ | alice | 改别人 | PATCH /user/1 |
✅ 403 Forbidden |
| ⑦ | alice | 改自己 | PATCH /user/2 age→99 |
✅ 200 OK |
| ⑧ | alice | 改角色 | PATCH /user/2 roleId→1 |
✅ 403 "普通用户不允许修改角色" |
🧠 功能权限 vs 数据权限
kotlin
┌──────────────────────────────────────────────────────────────────┐
│ 两层权限模型 │
├──────────────────────┬───────────────────────────────────────────┤
│ 功能权限 RBAC │ 数据权限(这次新增) │
│ "能调这个接口吗" │ "能看/改哪些数据" │
├──────────────────────┼───────────────────────────────────────────┤
│ Guard 层拦截 │ Service 层拦截 │
│ @Roles('admin') │ assertOwnOrAdmin(id, currentUser) │
│ 返回 401 / 403 │ 返回 403 "无权操作 User #X" │
│ │ │
│ 例:@Roles('admin')│ 例:alice 能调 GET /user │
│ 非 admin 根本 │ 但只能看到自己那条记录 │
│ 进不了接口 │ 想查别人 → 403 │
└──────────────────────┴───────────────────────────────────────────┘
🛡️ 核心防护代码
ts
// user.service.ts ------ 私有工具方法,所有 CRUD 都调它
private assertOwnOrAdmin(targetId: number, currentUser: CurrentUser): void {
if (currentUser.role !== 'admin' && currentUser.sub !== targetId) {
throw new ForbiddenException(
`无权操作 User #${targetId}(你只有权限操作自己的数据)`,
);
}
}
为什么放 Service 而不是 Controller? 想象未来有另一个 Controller(比如 AdminController)也想调 userService.findOne() 给后台看数据 ------ 放 Controller 里的话那条路径就绕过了。Service 层是最后一道防线,谁调都过不了。
🗡️ 额外加的两道保护
| 防护 | 代码位置 | 效果 |
|---|---|---|
| 普通用户不能改角色 | update() 里 if (dto.roleId && role !== 'admin') |
alice 不能给自己升 admin |
| 不能删自己 | remove() 里 if (id === currentUser.sub) |
admin 手滑也不会把自己删没 |
🎯 调用链一览(现在你的完整权限体系)
ini
请求 → JwtAuthGuard → RolesGuard → Controller → UserService
│ 有 token? │ @Roles? │ │
│ ❌ 401 │ ❌ 403 │ │
│ ✅ 继续 │ ✅ 继续 │ │
└───────────────┴──────────────┘ │
▼
assertOwnOrAdmin()
│ 当前用户 role 是 admin?
│ 或者 targetId === currentUser.sub?
│ ❌ 403 Forbidden
│ ✅ 放行,执行 DB 操作
└────────────────
你的 NestJS 现在有了工业级的完整权限体系 :功能权限 + 数据权限 + 密码加密 + JWT + 统一异常处理 + 统一响应 + 日志。下一步想玩什么?Redis 黑名单(强制下线)?或者多租户?