企业系统里"谁能做什么"是一道永恒的难题。我们在实际项目中遇到过这样的场景:同一个后台,普通员工只能查看自己的数据,部门主管可以导出报表,管理员才能操作用户。如果把权限判断散落在每个 Controller 里,代码会越来越难维护。
NestJS 的 Guard 机制配合自定义装饰器,能让权限逻辑集中、可复用,最终只需一行声明就能保护任意接口。
什么是 RBAC
RBAC(Role-Based Access Control,基于角色的访问控制)是最常见的权限模型:
- 用户拥有一个或多个角色(Role)
- 角色决定用户能访问哪些资源(Resource)
- 不直接给用户分配权限,而是通过角色间接赋予
常见角色示例:admin、manager、staff、guest。
NestJS Guard 的工作原理
Guard 是 NestJS 的一个切面,执行时机在中间件之后、拦截器之前。每个请求都会经过 Guard,Guard 返回 true 放行,返回 false 或抛出异常则拒绝。
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// 验证逻辑
return !!request.user;
}
}
Guard 可以作用于整个 Controller 或单个路由方法,也可以通过 APP_GUARD 注册为全局守卫。
第一步:自定义 @Roles() 装饰器
我们需要一种方式,在路由上声明"哪些角色可以访问"。NestJS 的 Reflector 允许我们把元数据附加到路由上,再在 Guard 里读取。
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
用法非常简洁:
@Get('reports')
@Roles('admin', 'manager')
getReports() {
return this.reportService.findAll();
}
第二步:实现 RolesGuard
Guard 里通过 Reflector 读取路由上附加的角色元数据,再和当前请求用户的角色做对比:
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 读取路由上声明的角色列表
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(), // 方法级
context.getClass(), // 类级(作为 fallback)
]);
// 没有声明角色限制,直接放行
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
// 从请求中取当前用户(由 JWT Guard 提前写入)
const { user } = context.switchToHttp().getRequest();
if (!user) return false;
// 判断用户角色是否满足要求
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
注意 getAllAndOverride 的优先级:方法级装饰器覆盖类级装饰器,这样可以在 Controller 层设置默认角色,再在某个方法上细化。
第三步:Guard 执行顺序
实际项目中通常有两个 Guard:先验证 Token 合法性(JWT Guard),再验证角色权限(Roles Guard)。执行顺序由注册顺序决定:
// app.module.ts
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard, // 先跑 JWT 验证
},
{
provide: APP_GUARD,
useClass: RolesGuard, // 再跑角色验证
},
],
JWT Guard 负责解析 Token、把用户信息写入 request.user;Roles Guard 再从 request.user 里读角色。两者职责分离,互不干扰。
公开路由的处理
全局注册 JWT Guard 后,登录接口自身也会被拦截。需要一个 @Public() 装饰器跳过验证:
// public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
在 JWT Guard 里检查这个标记:
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
// ... 正常 JWT 验证
}
登录接口只需加一行:
@Post('login')
@Public()
login(@Body() dto: LoginDto) { ... }
实际效果
经过以上设置,权限控制变成了纯声明式:
@Controller('users')
@UseGuards() // 全局 Guard 自动生效,无需手动加
export class UserController {
@Get()
@Roles('admin') // 仅管理员
findAll() { ... }
@Get(':id')
@Roles('admin', 'manager') // 管理员或主管
findOne() { ... }
@Put(':id/profile')
// 不加 @Roles,任意已登录用户可访问
updateProfile() { ... }
}
业务代码里不再出现任何 if (user.role !== 'admin') 的判断,权限策略一目了然。
小结
| 层次 | 职责 |
|---|---|
@Public() | 标记免验证路由 |
JwtAuthGuard | 验证 Token,填充 request.user |
@Roles() | 声明路由所需角色 |
RolesGuard | 读取元数据,比对用户角色 |
这套方案在我们的企业项目中稳定运行,新增接口时权限控制只需一行声明,代码审查时也一眼看出谁有权访问。如果后续需要更细粒度的资源级权限(比如"只能操作自己的数据"),可以在 Guard 或 Service 层再加一层策略判断。
作者:ekent · ek Studio 祎坤