nest(3)-jwt和RBAC

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

需要的东西:

  1. @nestjs/jwt 依赖(签发/验证 JWT)
  2. bcryptjs 依赖(密码加密)
  3. AuthService(登录/注册业务逻辑)
  4. JwtAuthGuard(验 token)
  5. @Public() 装饰器(标记免登录接口)
  6. @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,给所有公开方法加数据权限逻辑。让我想想哪些方法需要改:

  1. findAll() ------ admin 全部,user 只查自己
  2. findOne(id) ------ admin 全查,user 只能查自己的 id
  3. findOneWithRole(id) ------ 同上
  4. update(id) ------ admin 全改,user 只能改自己
  5. remove(id) ------ admin 全删,user 只能删自己
  6. findAllWithRole() ------ 同 findAll

create() 不用 ------ 注册就是所有人能创建

参数设计:currentUser?: { sub: number; name: string; role: string }

让我写出来。

Step 2:UserController ------ 加 RolesGuard + 透传 @CurrentUser

好,现在更新 UserController,给每个方法加 @CurrentUser() user 参数并传给 Service。注意 create 方法是 @Public() 的,不需要 currentUser。

Step 3:构建 + 测试数据权限

好,现在构建 + 启动 + 测试。数据权限的测试矩阵:

  1. admin → GET /user → 全部用户(2个)
  2. alice(user)→ GET /user → 只看到自己(1个)
  3. alice → GET /user/1(admin 的 id)→ 403 Forbidden
  4. alice → GET /user/2(自己)→ 200 OK
  5. admin → GET /user/2 → 200 OK
  6. alice → PATCH /user/2(改自己)→ 200 OK
  7. 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 黑名单(强制下线)?或者多租户?

相关推荐
遨翔在知识的海洋里1 小时前
nest(5)-Middleware
后端
程序员梅雨1 小时前
Linux & Shell 实用干货
linux·运维·服务器·后端·面试·php
大牧师2 小时前
TypeORM 入门教程
后端·sql·mysql·orm·nest·typeorm
IT_陈寒2 小时前
Vite静态资源路径这个大坑害我调了一下午
前端·人工智能·后端
oliver_sys_log2 小时前
绕过 Yearning 查询体验限制:我做了一个 DataGrip 只读 SQL 代理
后端·mysql
水深火乐2 小时前
golang-jwt v5 入门
后端
carson9552 小时前
基于springboot和vue的文本文件上传下载在线编辑功能
后端
n8n2 小时前
Spring AI 提示词工程进阶:System / User / Assistant 角色、Prompt Template 动态拼装与多角色人设切换
后端
步行cgn2 小时前
Spring Boot 主入口类上的 @Enable 和 @Scan 注解详解
java·spring boot·后端