4-11 重要完成RBAC守卫(RBAC闭环-企业级应用实现)
概述
本节是 RBAC 权限控制的收官之作,将前两块独立的逻辑——装饰器定义的权限标识与数据库中存储的用户角色权限——通过 Guard 串联起来,实现完整的权限控制闭环。
RBAC 闭环架构
┌─────────────────────────────────────────────────────────┐
│ Controller │
│ @Module('user') @Permission('read') │
│ ──────────────────────────────────── │
│ ↓ Reflector 读取 │
│ ┌─────────────────────────────────────┐ │
│ │ RolePermissionGuard │ │
│ │ │ │
│ │ 1. 读取装饰器中的权限标识 │ │
│ │ class + handler → "user:read" │ │
│ │ │ │
│ │ 2. 从 JWT 获取当前用户 │ │
│ │ req.user → username │ │
│ │ │ │
│ │ 3. 查询用户角色和权限 │ │
│ │ UserRepository → User → Roles │ │
│ │ RoleService → RolePermissions │ │
│ │ │ │
│ │ 4. 匹配权限 │ │
│ │ userPermissions.includes(rite) │ │
│ │ │ │
│ │ 5. Whitelist 超级管理员放行 │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
text
全局模块设计
Guard 中需要注入 UserRepository 和 RoleService,这要求将 UserModule 和 RoleModule 设计为全局模块:
// user.module.ts
import { Global, Module } from '@nestjs/common';
@Global()
@Module({
imports: [PrismaModule, RoleModule],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService, UserRepository], // 必须 export 才能 DI 注入
})
export class UserModule {}
typescript
注意事项:
- 全局模块不宜过多,只有被多个模块频繁使用的基础模块才应设为全局
- 全局模块在程序首次加载时需要初始化所有关联实例,过多会影响启动性能
- 全局模块的
exports是必须的,否则其他模块无法通过 DI 获取其 Provider
Guard 核心实现
1. 读取装饰器中的权限标识
// common/guards/role-permission.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { UserRepository } from '../../user/user.repository';
import { RoleService } from '../../role/role.service';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class RolePermissionGuard implements CanActivate {
constructor(
private reflector: Reflector,
private userRepository: UserRepository,
private roleService: RoleService,
private configService: ConfigService, // ConfigModule 是全局模块,可直接注入
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 读取模块级别和方法级别的装饰器元数据
const classPermission = this.reflector.get('permission', context.getClass());
const handlerPermission = this.reflector.get('permission', context.getHandler());
// 合并权限标识:处理 string 和 string[] 两种情况
const cls = Array.isArray(classPermission)
? classPermission.join(':')
: classPermission;
const handler = Array.isArray(handlerPermission)
? handlerPermission.join(':')
: handlerPermission;
// 拼接最终权限标识,如 "user:read"
const rite = `${cls}:${handler}`;
// ...后续逻辑
}
}
typescript
2. 获取当前用户并查询角色权限
async canActivate(context: ExecutionContext): Promise<boolean> {
// ...权限标识拼接
// 从 JWT 认证后的 request 对象获取用户信息
const req = context.switchToHttp().getRequest();
const { username } = req.user;
// 通过用户名查询用户及其角色
const user = await this.userRepository.findByUsername(username);
if (!user) return false;
// 提取用户所有角色 ID
const roleIds = user.userRoles.map((ur) => ur.roleId);
// 通过角色 ID 查询对应的权限
const roles = await this.roleService.findByIds(roleIds);
// 提取并扁平化所有权限名称
const permissions = roles
.flatMap((role) => role.rolePermissions)
.map((rp) => rp.permission.name);
// 去重
const userPermissions = [...new Set(permissions)];
// ...权限匹配
}
typescript
3. Whitelist 超级管理员机制
// .env
ROLE_ID_WHITELIST=2 # 超级管理员的角色 ID
typescript
async canActivate(context: ExecutionContext): Promise<boolean> {
// ...获取用户角色 IDs
// Whitelist 检查:超级管理员直接放行
const whitelist = this.configService.get<string>('ROLE_ID_WHITELIST');
if (whitelist) {
const whitelistArr = whitelist.split(',').map(Number);
const hasWhitelistRole = roleIds.some((id) => whitelistArr.includes(id));
if (hasWhitelistRole) return true;
}
// ...权限匹配
return userPermissions.includes(rite);
}
typescript
4. 完整 Guard 代码
@Injectable()
export class RolePermissionGuard implements CanActivate {
constructor(
private reflector: Reflector,
private userRepository: UserRepository,
private roleService: RoleService,
private configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const classPermission = this.reflector.get('permission', context.getClass());
const handlerPermission = this.reflector.get('permission', context.getHandler());
const cls = Array.isArray(classPermission) ? classPermission.join(':') : classPermission;
const handler = Array.isArray(handlerPermission) ? handlerPermission.join(':') : handlerPermission;
const rite = `${cls}:${handler}`;
const req = context.switchToHttp().getRequest();
const { username } = req.user;
const user = await this.userRepository.findByUsername(username);
if (!user) return false;
const roleIds = user.userRoles.map((ur) => ur.roleId);
// Whitelist 检查
const whitelist = this.configService.get<string>('ROLE_ID_WHITELIST');
if (whitelist) {
const whitelistArr = whitelist.split(',').map(Number);
if (roleIds.some((id) => whitelistArr.includes(id))) return true;
}
// 查询角色对应的权限
const roles = await this.roleService.findByIds(roleIds);
const userPermissions = [
...new Set(
roles.flatMap((role) => role.rolePermissions).map((rp) => rp.permission.name),
),
];
return userPermissions.includes(rite);
}
}
typescript
Controller 中的使用
// user.controller.ts
@Controller('user')
@Module('user') // 模块级权限标识
@UseGuards(AuthGuard('jwt'), RolePermissionGuard) // 认证 + 授权
export class UserController {
@Get()
@Permission('read') // 方法级权限标识 → 最终权限:user:read
findAll() {
return this.userService.findAll();
}
@Put(':id')
@Permission('update') // 最终权限:user:update
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
return this.userService.update(+id, dto);
}
}
typescript
多 Guard 的执行顺序
| 类型 | 执行顺序 |
|---|---|
| 装饰器 | 从下往上执行(最后写的最先执行) |
| Guard | 从前往后执行(先写的先执行) |
推荐写法:@UseGuards(AuthGuard('jwt'), RolePermissionGuard),确保先完成认证再进行权限校验。
RoleService 扩展方法
// role.service.ts
@Injectable()
export class RoleService {
constructor(private prismaClient: PrismaClient) {}
// 根据角色 ID 列表查询角色及其权限
async findByIds(ids: number[]) {
return this.prismaClient.role.findMany({
where: { id: { in: ids } },
include: {
rolePermissions: {
include: {
permission: true, // 包含完整的权限信息
},
},
},
});
}
}
typescript
模块间依赖关系
UserModule (Global)
├── imports: [RoleModule]
├── exports: [UserService, UserRepository]
│
RoleModule
├── exports: [RoleService]
│
ConfigModule (Global, 内置)
├── exports: [ConfigService]
text
模块导入链:UserModule 导入 RoleModule → RoleModule 导出 RoleService → Guard 中直接注入使用。
踩坑记录
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Guard 中无法注入 Service | 模块未设为 Global 或未 export | 添加 @Global() 装饰器并 export Provider |
| Whitelist 不生效 | .env 读取的是 string,roleId 是 number | 使用 .split(',').map(Number) 转换类型 |
| JWT 登录报 TypeError | findFirst 返回单个对象而非数组 | 不需要使用 [0] 索引 |
| 403 错误但用户应有权限 | 权限标识拼接错误 | 检查 class:handler 的格式是否与数据库中一致 |
RBAC 完整实现总结
整个 RBAC 实现涉及以下环节:
- 装饰器设计:定义模块级(
@Module)和方法级(@Permission)的权限标识字符串 - 数据模型:User → UserRole → Role → RolePermission → Permission 的多对多关联
- CRUD 操作:角色和权限的增删改查,注意嵌套关联的先删后建模式
- Guard 实现:读取装饰器标识 → 查询用户权限 → 匹配判断
- Whitelist 机制:超级管理员绕过权限检查,方便初始化系统数据
扩展方向:基于策略(Policy-based)的权限控制,使用 CASL 库实现更细粒度的字段级权限控制。NestJS 官方文档中称之为"与 CASL 集成"。
↑