白留白WHITE SPACE
返回所有文章
NestJS / 后端开发

NestJS 实战 06:Guard、Decorator 与 RBAC 权限控制

在“已登录”的基础上增加“有权限才可访问”。定义角色模型:user、moderator、admin。实现管理员改角色、版主删帖等受控接口。

本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。

1. 这节课要实现的功能是什么

  • 在“已登录”的基础上增加“有权限才可访问”。
  • 定义角色模型:user、moderator、admin。
  • 实现管理员改角色、版主删帖等受控接口。

2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)

2.1 先理解 Decorator:它在 NestJS 里是什么

Decorator(装饰器)可以理解成“给类、方法、参数附加信息的语法”。
Nest 会在运行时读取这些信息,然后决定这个类、这个方法、这个参数应该怎样被框架处理。

所以装饰器的价值不在于“写法简洁”,而在于:让你用声明式方式描述规则,而不是把规则硬写在业务代码里。

2.2 NestJS 中常见的 Decorator 类型

你可以先把 Nest 的装饰器分成四类:

  • 类装饰器:描述一个类在框架中的角色,例如 @Module()、@Controller()、@Injectable()
  • 方法装饰器:描述一个方法该如何被处理,例如 @Get()、@Post()、@UseGuards()
  • 参数装饰器:描述参数从哪里来,例如 @Body()、@Param()、@Req()、@CurrentUser()
  • 元数据装饰器:给类或方法贴上额外规则,供 Guard、Interceptor 等运行时读取,例如 @Roles()

2.3 什么时候适合自己写 Decorator

适合自己写 Decorator 的场景通常有两类:

  1. 有重复的参数提取逻辑
    例如每个接口都要写 req.user,这时可以抽成 @CurrentUser()

  2. 有声明式规则要写在接口上
    例如角色、权限、白名单、资源标签,这时可以抽成 @Roles()、@Public() 这种元数据装饰器

但要注意:Decorator 主要负责“声明”,不负责承载复杂执行逻辑。

  • @Roles('admin') 负责声明“这个接口需要 admin”
  • 真正执行权限判断的是 RolesGuard

2.4 认证和授权的区别

  • 认证(Authentication):你是谁(是否登录);
  • 授权(Authorization):你能做什么(是否有权限)。

它们通常通过不同 Guard 完成,并且可以组合使用。

2.5 如何自定义 Guard

很多同学第一次接触 Guard 时,会以为只能用 Nest 现成的能力。其实不是。
只要你实现了 CanActivate 接口,并写好 canActivate() 方法,这个类就是一个自定义 Guard。

最小写法通常是这样:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class DemoGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return !!request.headers['x-demo-token'];
  }
}

这里的核心只有三件事:

  • @Injectable():让这个 Guard 能被 Nest 管理;
  • implements CanActivate:声明这是一个 Guard;
  • canActivate():定义“这个请求能不能继续执行”。

所以自定义 Guard 的本质不是“重新发明一套认证机制”,而是把你的访问控制规则写进 canActivate()。

2.6 什么时候适合自己写 Guard

适合自定义 Guard 的场景通常是:

  • 判断某个请求头是否存在;
  • 判断当前用户是否拥有某种业务权限;
  • 判断资源是否属于当前用户;
  • 判断是否命中白名单、灰度、开关策略。

换句话说,只要你有“访问前必须先判断”的规则,就可以考虑用 Guard。

2.7 @Roles() 与 Reflector

@Roles() 把角色要求写在接口上,RolesGuard 在运行时用 Reflector 读取元数据并做判断。

2.8 @CurrentUser() 的价值

把 request.user 提取为可复用装饰器,避免每个 Controller 重复写取值逻辑。

2.9 组合 Guard 的执行思路

典型顺序:JwtAuthGuard 先恢复用户,RolesGuard 再判断角色。

3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)

3.1 src/auth/decorators/roles.decorator.ts

import { SetMetadata } from '@nestjs/common';
import type { UserRole } from '../../users/users.repository';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);

3.2 src/auth/guards/roles.guard.ts

RolesGuard 本身就是一个标准的自定义 Guard。
它并不是 Nest 内置的 Guard,而是项目自己实现的权限判断器:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import type { UserRole } from '../../users/users.repository';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles || requiredRoles.length === 0) return true;

    const request = context.switchToHttp().getRequest<{ user?: { role?: UserRole } }>();
    const role = request.user?.role;
    return !!role && requiredRoles.includes(role);
  }
}

你可以把它拆成三步理解:

  1. 读取 @Roles() 写入的方法元数据;
  2. 从 request.user 中取出当前用户角色;
  3. 判断角色是否满足要求,最后返回 true 或 false。

3.3 src/auth/decorators/current-user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import type { AuthUser } from '../types/auth-user';

export const CurrentUser = createParamDecorator(
  (_data: unknown, context: ExecutionContext): AuthUser => {
    const request = context.switchToHttp().getRequest<{ user: AuthUser }>();
    return request.user;
  },
);

3.4 src/users/users.controller.ts - 受控接口示例(管理员改角色)

@Patch(':id/role')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
async updateRole(
  @Param('id', ParseIntPipe) id: number,
  @Body() dto: UpdateUserRoleDto,
  @CurrentUser() currentUser: { userId: number },
) {
  if (currentUser.userId === id && dto.role !== 'admin') {
    throw new ForbiddenException('Admin cannot remove own admin role');
  }
  return this.usersService.updateRole(id, dto.role);
}

3.5 src/threads/admin-threads.controller.ts - 受控接口示例(版主删帖)

@Controller('admin/threads')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminThreadsController {
  @Delete(':id')
  @Roles('moderator', 'admin')
  remove(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: AuthUser) {
    return this.threadsService.removeByModerator(id, user.userId);
  }
}

4. 总结这节课知识点

  • 你完成了认证到授权的升级。
  • 你知道了 Guard 不只是内置能力,也可以通过实现 CanActivate 自定义。
  • 你掌握了元数据装饰器 + Guard 的权限实现模式。
  • 你可以在不污染业务代码的前提下扩展复杂权限规则。

5. 每节课分支

  • lesson-06-guards-rbac
  • 远程仓库:https://github.com/Shiuwn/nestjs-tutorial.git

返回课程目录

上一课:注册登录与 JWT 鉴权

下一课:BBS 核心域模型(板块、主题、回复)

— 感谢阅读 —