第 18 章:RBAC 权限
学习目标
- 设计 RBAC 数据模型
- 写 RolesGuard 和 @Roles 装饰器
- 实现细粒度权限
- 避开 3 个权限设计坑
一、RBAC 模型
| 概念 | 含义 |
|---|---|
| User | 用户 |
| Role | 角色(admin/user/guest) |
| Permission | 权限(user:create、user:delete) |
| UserRole | 用户 ↔ 角色 |
| RolePermission | 角色 ↔ 权限 |
sql
users(id, username)
roles(id, name)
permissions(id, code) -- 'user.create'
user_roles(user_id, role_id)
role_permissions(role_id, permission_id)二、实体定义
typescript
// role.entity.ts
@Entity()
export class Role {
@PrimaryGeneratedColumn() id: number;
@Column({ unique: true }) name: string;
@ManyToMany(() => Permission)
@JoinTable({ name: 'role_permissions' })
permissions: Permission[];
}
// permission.entity.ts
@Entity()
export class Permission {
@PrimaryGeneratedColumn() id: number;
@Column({ unique: true }) code: string; // 'user.create'
}
// user.entity.ts(加多对多)
@ManyToMany(() => Role)
@JoinTable({ name: 'user_roles' })
roles: Role[];三、@Roles 装饰器
typescript
// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);四、RolesGuard
typescript
// guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
ctx.getHandler(), ctx.getClass(),
]);
if (!required) return true; // 没标 @Roles 就放行
const { user } = ctx.switchToHttp().getRequest();
if (!user) throw new UnauthorizedException();
const userRoles = (user.roles ?? []).map(r => r.name);
return required.some(r => userRoles.includes(r));
}
}挂载:
typescript
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard) // 先 JWT,再 RBAC
export class AdminController {
@Get('users')
@Roles('admin')
list() { /* ... */ }
}⚠️ 坑 1:Guard 顺序写反 → 未登录就能访问 admin,先 JwtAuthGuard 再 RolesGuard。
五、细粒度权限(@Permissions)
typescript
export const PERMISSIONS_KEY = 'permissions';
export const Permissions = (...codes: string[]) =>
SetMetadata(PERMISSIONS_KEY, codes);
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
ctx.getHandler(), ctx.getClass(),
]);
if (!required) return true;
const { user } = ctx.switchToHttp().getRequest();
const userPerms = (user.roles ?? []).flatMap(r =>
(r.permissions ?? []).map(p => p.code),
);
return required.every(p => userPerms.includes(p)); // 必须全部满足
}
}用法:
typescript
@Post()
@Permissions('user.create')
create() {}
@Delete(':id')
@Permissions('user.delete')
remove() {}六、初始化种子数据
bash
pnpm add @nestjs/seedtypescript
// seeds/init.seed.ts
export default class InitSeed implements Seeder {
async run(dataSource: DataSource): Promise<void> {
// ① 拿 permissions 表的 Repository
const permRepo = dataSource.getRepository(Permission);
// ② 先建 3 条权限(基础数据,后面角色挂这些权限)
await permRepo.save([
{ code: 'user.create' }, // 创建用户的权限
{ code: 'user.update' }, // 更新用户的权限
{ code: 'user.delete' }, // 删除用户的权限
]);
// ③ 拿 roles 表的 Repository
const roleRepo = dataSource.getRepository(Role);
// ④ 创建 admin 角色(第 1 次 save,只插 name,permissions 还是空)
const admin = await roleRepo.save({ name: 'admin' });
// ⑤ 把刚建的 3 个权限全查出来,赋给 admin.permissions
admin.permissions = await permRepo.find();
// ⑥ 第 2 次 save:TypeORM 检测到 ManyToMany 字段变化,
// 自动往中间表 role_permissions 写 3 条关联记录
await roleRepo.save(admin);
}
}关键点:
| 步骤 | 在干啥 | 数据库变化 |
|---|---|---|
② permRepo.save([...]) | 插 3 条权限 | permissions 表 +3 行 |
④ roleRepo.save({ name: 'admin' }) | 插 1 个角色 | roles 表 +1 行,中间表还没数据 |
⑤ admin.permissions = ... | 内存赋值 | 数据库还没变 |
⑥ roleRepo.save(admin) | 二次 save | TypeORM 检测到 ManyToMany 变化 → 自动写中间表 |
为什么必须 save 两次,不能一次搞定?
typescript
// ❌ 想一步到位?不行
const admin = await roleRepo.save({
name: 'admin',
permissions: [{ code: 'user.create' }, ...], // 这种嵌套写法 TypeORM 不认
});- TypeORM 的
save()对 ManyToMany 嵌套数组的处理不可靠(尤其跨实体时) - 稳妥做法:先建角色 → 再赋关系 → 再 save 一次(让中间表通过关系字段显式写入)
完整数据流向:
执行前:permissions=[], roles=[], role_permissions=[]
② 保存 → permissions: [user.create, user.update, user.delete]
④ 保存 → roles: [admin] ⚠️ 中间表还空
⑤ 赋值 → admin.permissions = [3 个 Permission 实体] (内存操作)
⑥ 保存 → role_permissions: [(admin, user.create), (admin, user.update), (admin, user.delete)]
执行后:admin 角色拥有全部 3 个权限 ✅七、数据库查询带权限
typescript
@Get()
list(@CurrentUser() user: any) {
return this.repo.find({
where: { id: In(user.ownedIds ?? []) }, // 看不到别人的
});
}⚠️ 坑 2:只做路由级权限,数据查询不限制 → 用户能看到别人的订单。
八、Ownership 资源归属
典型场景:个人文章编辑/删除,只许本人操作自己的资源。
typescript
@Controller('articles')
export class ArticlesController {
@Get(':id') article() {} // 公开
@Put(':id') @UseGuards(OwnershipGuard) update() {} // 只能本人改自己
@Delete(':id') @UseGuards(OwnershipGuard) remove() {} // 只能本人删自己
}对比前两个 Guard:
| Guard | 解决什么 | 判断依据 |
|---|---|---|
| RolesGuard | "你是 admin 吗?" | 角色字符串 |
| PermissionsGuard | "你会这个能力吗?" | 权限 code |
| OwnershipGuard | "这资源是你的吗?" | resource.userId === req.user.id |
typescript
@Injectable()
export class OwnershipGuard implements CanActivate {
async canActivate(ctx: ExecutionContext): Promise<boolean> {
const req = ctx.switchToHttp().getRequest();
const id = +req.params.id;
const resource = await this.svc.findOne(id);
if (resource.userId !== req.user.id) {
throw new ForbiddenException();
}
return true;
}
}执行链路(用户 A 想删用户 B 的文章 id=101):
DELETE /articles/101
Authorization: Bearer <token: userId=1>
↓
const id = +req.params.id; // 101
const resource = await this.svc.findOne(101); // { id:101, userId:2, ... }
if (resource.userId !== req.user.id) { // 2 !== 1 → 命中
throw new ForbiddenException(); // ❌ 403
}完整 Guard 串联(角色权限矩阵里的 PUT /users/:id —— "admin 或本人"):
typescript
@Put(':id')
@UseGuards(JwtAuthGuard, RolesGuard, OwnershipGuard)
update() {}
// ① JwtAuthGuard → 登录了吗?
// ② RolesGuard → 是 admin?(放行)
// 或者不是 admin ↓
// ③ OwnershipGuard → resource.userId === req.user.id?(放行)
// 否则 ❌ 403为什么 RolesGuard 不够? 普通用户也能进 RolesGuard("user 角色"通常没 @Roles 限制),没 OwnershipGuard 就能改/删别人资源。
⚠️ 坑 3:Ownership 检查但没缓存 → 每个请求都查库,性能差,可加 Redis。
九、组合元数据
typescript
// @Roles + @Permissions 组合用(装饰器层)
@Roles('admin')
@Permissions('user.delete')
@Delete(':id')
remove() {}完整写法(Guard 层也要写上,跟装饰器配套):
typescript
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard, PermissionsGuard, OwnershipGuard)
export class UsersController {
@Delete(':id')
@Roles('admin') // 装饰器 ① → RolesGuard 读
@Permissions('user.delete') // 装饰器 ② → PermissionsGuard 读
remove() {}
}两层的分工:
| 层级 | 写在哪 | 谁来读 |
|---|---|---|
装饰器层 @Roles(...) @Permissions(...) | 方法上 | RolesGuard / PermissionsGuard 用 Reflector 读 |
守卫层 @UseGuards(...) | 类上 | NestJS 按顺序执行 |
执行流程:
DELETE /users/123 Bearer <token>
↓
① JwtAuthGuard → 验 token,挂 req.user
↓
② RolesGuard → @Roles('admin') 在用户角色里?
↓
③ PermissionsGuard → @Permissions('user.delete') 权限齐全?
↓
④ OwnershipGuard → resource.userId === req.user.id?
↓
Controller.remove() → 删完整 Guard 串联:JwtAuthGuard → RolesGuard → PermissionsGuard → OwnershipGuard。
十、实战:角色权限矩阵
| 路由 | 角色 |
|---|---|
GET /users | admin, manager |
POST /users | admin |
PUT /users/:id | admin 或本人 |
DELETE /users/:id | admin |
typescript
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
@Get() @Roles('admin', 'manager') list() {}
@Post() @Roles('admin') create() {}
@Put(':id') @Roles('admin') update() {}
@Delete(':id') @Roles('admin') remove() {}
}十一、本章小结
| 要点 | 关键 |
|---|---|
| 模型 | User-Role-Permission 三层 |
| Guard | RolesGuard + Reflector.getAllAndOverride |
| 装饰器 | @Roles(...names) + @Permissions(...codes) |
| 顺序 | JwtAuthGuard → RolesGuard → PermissionsGuard |
| 数据级 | 路由权限 + 查询条件,不能只看路由 |
| Ownership | 资源归属检查(本人或管理员) |
动手练习
- 角色 Guard:写
RolesGuard+@Roles('admin') - 权限 Guard:写
PermissionsGuard+@Permissions('user.create') - Ownership:写
OwnershipGuard,只允许本人修改自己的文章
下一章:第 19 章:Swagger 文档 →