NestJS中Passport JWT认证下如何在RoleGuard获取用户角色数据
核心认知纠正
首先明确:NestJS 的 Guard 原生支持异步逻辑,canActivate方法可以直接返回Promise<boolean>或Observable<boolean>,你完全可以在守卫里执行数据库查询等异步操作,不存在异步调用无法在守卫中运行的限制。
两种可落地的实现方案
方案1:守卫内直接查库校验角色(优先推荐)
这是安全性最高、权限实时性最好的方案,实现逻辑如下:
- 先写一个自定义角色装饰器,用来标记接口需要的角色权限
// roles.decorator.ts import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); - 实现RoleGuard,从请求上拿到JWT解析后挂载的用户ID,异步查库获取最新角色,和接口要求的角色做匹配
// role.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from '../user/user.entity'; import { ROLES_KEY } from './roles.decorator'; @Injectable() export class RolesGuard implements CanActivate { constructor( private reflector: Reflector, @InjectRepository(User) private userRepository: Repository<User> ) {} async canActivate(context: ExecutionContext): Promise<boolean> { // 读取当前接口/控制器上配置的要求角色 const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); // 未配置角色限制直接放行 if (!requiredRoles?.length) return true; const req = context.switchToHttp().getRequest(); const userId = req.user?.id; if (!userId) return false; // 查库获取用户最新关联的角色,可根据自己的表结构调整查询逻辑 const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['roles'] }); if (!user) return false; // 只要用户持有任意一个要求的角色就放行 return requiredRoles.some(requiredRole => user.roles.some(ur => ur.name === requiredRole) ); } } - 使用的时候注意把JWT认证守卫放在RolesGuard前面,保证
req.user已经被赋值:@UseGuards(JwtAuthGuard, RolesGuard) @Controller('admin') export class AdminController { @Roles('admin') @Get('dashboard') getDashboard() { return 'admin only content' } }
方案优缺点
- 优点:角色权限实时生效,用户角色调整后不需要等JWT过期,下次请求就会应用新权限,不会出现权限不一致的安全问题。
- 缺点:每个加了角色校验的接口会多一次数据库查询,QPS高的场景可以加一层Redis缓存(key为用户ID,value为角色列表,设置合理过期时间,角色变更时主动删除对应缓存)平衡性能和实时性。
方案2:角色信息存入JWT Payload
这个方案完全可行,适合角色变更频率极低、对性能要求极高的场景。
实现逻辑:
- 登录签发JWT的时候,把查询到的用户角色直接写入payload
// auth.service.ts 登录逻辑片段 async validateUser(username: string, password: string) { // 省略密码校验逻辑 const user = await this.userRepository.findOne({where: {username}, relations: ['roles']}) const payload = { sub: user.id, username: user.username, roles: user.roles.map(r => r.name) // 角色写入payload } return { access_token: this.jwtService.sign(payload) } } - JWT策略校验通过后,会自动把payload里的所有字段(包括roles)挂载到
req.user上,RolesGuard不需要查库,直接读取req.user.roles做校验即可,去掉方案1里的查库逻辑就行。
方案优缺点
- 优点:不需要额外查库,守卫执行效率高,逻辑简单。
- 缺点:权限不实时,用户角色变更后,必须等旧JWT过期重新登录拿到新token,新角色才会生效;如果要做权限主动失效,还需要额外加token黑名单或者缓存校验,复杂度反而上升。另外要注意:JWT的Payload只是Base64编码,没有加密,不要把敏感信息存在Payload里。
选型建议
- 涉及敏感操作、后台管理类的系统优先选方案1,性能问题可以靠缓存解决,安全优先级更高。
- 面向C端、角色极少变更、QPS极高的场景可以选方案2,降低数据库压力。
内容的提问来源于stack exchange,提问作者CAoT
相关产品推荐
相关产品推荐

