FORMA

守卫 (Guards) 与授权

守卫决定是否放行请求,常用于认证与授权。见 auth

守卫负责在请求到达控制器处理函数之前,决定是否让该请求继续执行。常用于身份验证授权(例如检查用户是否已登录、是否具有特定角色或权限)。守卫返回 true 表示允许访问,返回 false 或抛出异常则拒绝请求。

一、守卫的作用与接口

守卫实现 CanActivate 接口,该接口只有一个方法 canActivate(context: ExecutionContext),返回 booleanPromise<boolean>Observable<boolean>

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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization;
    // 简单示例:验证 token 是否存在
    return !!token;
  }
}

如果守卫返回 false,NestJS 会抛出 ForbiddenException(可被异常过滤器捕获并转换为 403 响应)。

二、内置守卫

NestJS 核心库并没有提供具体的守卫实现,但官方提供了 @nestjs/passport 模块,它与 Passport.js 集成,实现了常见的身份认证守卫(如 JWT、Local、OAuth 等)。你可以基于它快速构建认证守卫。

例如使用 @nestjs/jwt@nestjs/passport 创建 JwtAuthGuard

ts
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}

这个守卫会自动解析请求中的 JWT token 并验证其有效性,如果失败则返回 401

在需要认证的路由上使用:

ts
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Req() req) {
  return req.user;
}

三、使用守卫

1. 方法级或控制器级守卫

使用 @UseGuards() 装饰器,可以同时应用多个守卫(顺序执行,全部通过才放行)。

ts
@Controller('users')
@UseGuards(AuthGuard)          // 控制器级,所有方法生效
export class UsersController {
  @Get()
  findAll() { ... }

  @Post()
  @UseGuards(AdminGuard)       // 方法级,覆盖或叠加控制器守卫(按顺序执行)
  create() { ... }
}

2. 全局守卫

全局守卫应用于所有路由,可通过 app.useGlobalGuards 注册,或通过依赖注入方式(APP_GUARD 令牌)注册以便守卫使用其他服务。

方式一(无依赖注入):

ts
// main.ts
app.useGlobalGuards(new AuthGuard());

方式二(支持依赖注入,推荐):

ts
// 任意模块(通常是 AppModule)
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { AuthGuard } from "./auth.guard";

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AppModule {}

这样注册的全局守卫可以注入模块内的其他服务(如 ConfigServiceUserService)。

四、守卫与中间件的区别

特性中间件守卫
职责通用前置处理(日志、解析、跨域等)专门负责鉴权/授权
可否感知执行上下文只能访问请求/响应对象,不知道哪个控制器/方法将被调用可通过 ExecutionContext 获取目标控制器类、方法及其元数据
可否返回 false不能拦截请求(只能调用 next() 或抛异常)可以返回 false 来拒绝请求
依赖注入范围只能通过模块的 configure 方法注册,支持依赖注入与普通提供者相同,可灵活注入

关键点:守卫可以读取控制器或方法上通过 @SetMetadata 设置的自定义元数据(例如角色列表),从而做出更精细的权限判断。

五、通过反射获取自定义元数据

1. 使用 @SetMetadata() 设置元数据

ts
@Controller('admin')
@SetMetadata('roles', ['admin'])
export class AdminController {
  @Get()
  @SetMetadata('permissions', ['read', 'write'])
  getData() { ... }
}

2. 创建自定义装饰器(推荐)

通常我们封装 @Roles() 装饰器,隐藏 @SetMetadata 的实现细节。

ts
import { SetMetadata } from "@nestjs/common";

export const Roles = (...roles: string[]) => SetMetadata("roles", roles);

使用:

ts
@Roles('admin')
@Get()
findAll() { ... }

3. 守卫中读取元数据

通过 Reflector 工具类读取。

ts
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>("roles", context.getHandler());
    if (!roles) return true; // 没有角色要求,直接放行

    const request = context.switchToHttp().getRequest();
    const user = request.user; // 假设已经通过认证守卫挂载了 user 对象
    return roles.some((role) => user.roles?.includes(role));
  }
}

注意Reflector.get 还可以通过 context.getClass() 获取控制器类上的元数据,可用于方法级与类级元数据合并。

六、守卫上下文:HTTP、GraphQL、WebSocket

ExecutionContext 是守卫的强大之处,它不仅能处理 HTTP 请求,还能用于 GraphQL 或 WebSocket 场景。

1. 区分上下文类型

通过 context.getType<'http' | 'ws' | 'rpc'>() 判断当前请求类型。

ts
canActivate(context: ExecutionContext): boolean {
  const type = context.getType();
  if (type === 'http') {
    const request = context.switchToHttp().getRequest();
    // HTTP logic
  } else if (type === 'ws') {
    const client = context.switchToWs().getClient();
    // WebSocket logic
  } else if (type === 'rpc') {
    const data = context.switchToRpc().getData();
    // Microservice logic
  }
  return true;
}

2. GraphQL 场景

在 GraphQL 解析器中,守卫可以通过 context.getArgByIndex() 等获取参数,但更常见的是从 context 参数中获取请求信息。

ts
// 在 GraphQL 模块中,执行上下文可以这样获取请求对象
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;

七、综合示例:基于 JWT 认证 + 角色授权

ts
// jwt-auth.guard.ts (基于 passport)
export class JwtAuthGuard extends AuthGuard('jwt') {}

// roles.guard.ts
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(ctx: ExecutionContext) {
    const requiredRoles = this.reflector.get<string[]>('roles', ctx.getHandler());
    if (!requiredRoles) return true;
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    return requiredRoles.some(role => user.roles?.includes(role));
  }
}

// 控制器
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
export class AdminController {
  @Get()
  getData() { ... }
}

总结

概念说明
守卫核心实现 CanActivate 接口,返回布尔值决定是否放行
使用方式@UseGuards() 方法/控制器级;app.useGlobalGuardsAPP_GUARD 全局
与中间件区别守卫可感知执行上下文和自定义元数据,更适于授权
自定义元数据@SetMetadataReflector.get 读取;可封装装饰器简化
跨平台支持通过 ExecutionContextgetTypeswitchTo* 适配 HTTP/WS/Microservice

通过合理组合守卫和反射,可以实现灵活、可维护的权限控制体系。

参考文献

以下链接在编写时均可正常访问:

资料说明
NestJS 文档官方
Guards本章主题
Request lifecycle执行顺序

Series

new

11 / 19