RBAC 权限控制
一、权限模型概述
RBAC(Role-Based Access Control) 是最常用的权限模型:
用户 → 角色 → 权限
User → Role → Permission
示例:
alice → ADMIN → [read:user, create:user, delete:user, read:post, ...]
bob → EDITOR → [read:post, create:post, update:post]
carol → USER → [read:post]NestJS 中 RBAC 通过元数据(Metadata)+ 守卫(Guard)+ 装饰器(Decorator) 三者配合实现。
二、基础角色系统
定义角色枚举
typescript
// common/enums/role.enum.ts
export enum Role {
USER = 'user',
MODERATOR = 'moderator',
ADMIN = 'admin',
}角色装饰器
typescript
// common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);角色守卫
typescript
// common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../enums/role.enum';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 读取方法或类上的 @Roles() 元数据
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(), // 方法级别优先
context.getClass(), // 类级别兜底
]);
// 没有设置角色限制,放行(任何已认证用户都可访问)
if (!requiredRoles || requiredRoles.length === 0) return true;
const { user } = context.switchToHttp().getRequest();
// 未认证用户(应先被 JwtAuthGuard 拦截,这里是保底)
if (!user) return false;
return requiredRoles.includes(user.role);
}
}三、公开接口与全局守卫
typescript
// common/decorators/public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);typescript
// common/guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
// @Public() 标记的路由跳过 JWT 验证
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
const message = info?.name === 'TokenExpiredError'
? 'Token 已过期'
: '请先登录';
throw new UnauthorizedException(message);
}
return user;
}
}typescript
// app.module.ts:全局注册两个守卫
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard }, // 所有路由默认需认证
{ provide: APP_GUARD, useClass: RolesGuard }, // 需要角色时检查
],四、控制器中的使用
typescript
@ApiTags('用户管理')
@Controller('users')
export class UsersController {
// 获取自己的信息(任何已登录用户)
@Get('me')
getProfile(@CurrentUser() user: UserEntity) {
return user;
}
// 获取所有用户(管理员才能)
@Roles(Role.ADMIN)
@Get()
findAll() {
return this.usersService.findAll();
}
// 删除用户(管理员才能)
@Roles(Role.ADMIN)
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id);
}
// 公开接口(不需要登录)
@Public()
@Get('count')
countUsers() {
return this.usersService.count();
}
}五、细粒度权限控制(CASL)
基础 RBAC 只能控制角色,无法表达"用户只能编辑自己的文章"这类资源级权限。使用 CASL:
bash
npm install @casl/ability @casl/prisma # 或 @casl/typeorm定义能力
typescript
// ability/ability.factory.ts
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
export type Actions = 'manage' | 'create' | 'read' | 'update' | 'delete';
export type Subjects = 'Post' | 'User' | 'Comment' | 'all';
export type AppAbility = ReturnType<typeof createMongoAbility<[Actions, Subjects]>>;
@Injectable()
export class AbilityFactory {
defineAbility(user: { id: number; role: Role }) {
const { can, cannot, build } = new AbilityBuilder(
createMongoAbility<[Actions, Subjects]>
);
if (user.role === Role.ADMIN) {
can('manage', 'all'); // 管理员可以做一切
} else if (user.role === Role.MODERATOR) {
can('read', 'Post');
can('update', 'Post');
can('delete', 'Post');
can('read', 'User');
cannot('delete', 'User'); // 不能删除用户
} else {
// 普通用户
can('read', 'Post');
can('create', 'Post');
// 只能修改自己的文章(条件规则)
can('update', 'Post', { authorId: user.id });
can('delete', 'Post', { authorId: user.id });
can('update', 'User', { id: user.id }); // 只能改自己的资料
}
return build({
detectSubjectType: (item: any) => item.constructor?.name ?? item,
});
}
}守卫检查能力
typescript
// ability/abilities.guard.ts
export type PolicyHandler = (ability: AppAbility) => boolean;
export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
SetMetadata(CHECK_POLICIES_KEY, handlers);
@Injectable()
export class PoliciesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private abilityFactory: AbilityFactory,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const policyHandlers = this.reflector.get<PolicyHandler[]>(
CHECK_POLICIES_KEY,
context.getHandler(),
);
if (!policyHandlers) return true;
const { user } = context.switchToHttp().getRequest();
const ability = this.abilityFactory.defineAbility(user);
return policyHandlers.every(handler => handler(ability));
}
}使用细粒度权限
typescript
@Controller('posts')
export class PostsController {
// 只有有 create:Post 权限才能创建
@CheckPolicies((ability) => ability.can('create', 'Post'))
@Post()
create(@Body() dto: CreatePostDto, @CurrentUser() user: User) {
return this.postsService.create(dto, user.id);
}
// 删除文章:需要拿到文章实体再判断(Service 层检查)
@Delete(':id')
async remove(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User) {
const post = await this.postsService.findOne(id);
const ability = this.abilityFactory.defineAbility(user);
if (!ability.can('delete', post)) {
throw new ForbiddenException('无权删除此文章');
}
return this.postsService.remove(id);
}
}六、组合装饰器(减少样板代码)
typescript
// common/decorators/auth.decorator.ts
import { applyDecorators, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiUnauthorizedResponse, ApiForbiddenResponse } from '@nestjs/swagger';
export function Auth(...roles: Role[]) {
return applyDecorators(
roles.length > 0 ? Roles(...roles) : () => {},
UseGuards(JwtAuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: '未登录或 Token 已过期' }),
roles.length > 0
? ApiForbiddenResponse({ description: `需要 ${roles.join('/').toUpperCase()} 角色` })
: () => {},
);
}
// 使用
@Auth() // 任何已登录用户
@Get('me')
getProfile() {}
@Auth(Role.ADMIN) // 仅管理员
@Delete(':id')
remove() {}
@Auth(Role.ADMIN, Role.MODERATOR) // 管理员或版主
@Put(':id/publish')
publishPost() {}七、数据库驱动的动态权限
角色和权限存数据库,支持运行时动态配置:
typescript
// 数据库设计
// users → user_roles → roles → role_permissions → permissions
// permissions 表:{ id, action: 'create', resource: 'post' }
// roles 表:{ id, name: 'editor' }
// role_permissions:角色与权限多对多中间表
@Injectable()
export class DynamicRolesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private permissionsService: PermissionsService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const required = this.reflector.get<{ action: string; resource: string }>(
'permission',
context.getHandler(),
);
if (!required) return true;
const { user } = context.switchToHttp().getRequest();
// 从数据库加载用户的所有权限
const userPermissions = await this.permissionsService.getUserPermissions(user.id);
return userPermissions.some(
p => p.action === required.action && p.resource === required.resource,
);
}
}八、最佳实践
- 全局守卫默认拦截,白名单放行:
@Public()比每个接口加@UseGuards更安全 - 角色继承:用数组包含判断,
[Role.ADMIN, Role.MODERATOR]优于嵌套 if - 资源级权限用 CASL:判断"是否拥有者"等条件权限,不要在 Guard 里查数据库
- 权限检查放 Service 层:Guard 负责认证,Service 负责业务权限验证(可测试性更好)
- 不要在 JWT payload 存完整权限列表:Token 会变大,且权限更改需要 Token 过期才生效;从数据库实时查更安全
可运行 Demo:
practice/04-auth-system— RolesGuard + @Roles 装饰器,三级角色权限验证
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
RolesGuard 总是放行 | Reflector.getAllAndOverride 返回 undefined | 检查 @Roles() 装饰器是否正确加在 Handler 或 Class 上 |
CASL ability.can() 返回 false 但应该通过 | 规则中 subject 类型字符串拼写错误 | 用 subject('Post', post) 替代裸字符串,避免拼写错误 |
| 动态权限未实时生效 | 权限从 Token payload 读取,Token 未刷新 | 动态权限应从数据库实时查询,而不是缓存在 Token 中 |