返回文章列表 →

Note

守卫

nestjs的守卫介绍

发布于 更新于

守卫

用于判断当前请求是否有资格进入这个路由

位置和流程

守卫在所有中间件之后、拦截器和管道之前执行。因此,守卫拒绝请求后,后面的 Pipe 、Controller 和 Service 都不会执行

客户端请求
   ↓
Middleware 中间件
   ↓
Guard 守卫
   ↓
Interceptor 拦截器(进入阶段)
   ↓
Pipe 管道
   ↓
Controller
   ↓
Service
   ↓
Interceptor 拦截器(返回阶段)
   ↓
客户端响应

常见作用

  • 认证:判断用户是否登录、验证 JWT 是否有效、判断用户角色
  • 鉴权:判断用户是否具有某项权限

定义守卫

使用 @Injectable() 修饰并且实现 CanActivate 接口,这个类就是守卫。守卫内部必须实现 canActivate 方法。守卫通常命名为 xxx.guard.ts

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

@Injectable()
// 守卫类AuthGuard
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    return true;
  }
}

ExecutionContext,在Argumenthost讨论过,它可以获取请求对象、当前Controller、当前路由方法

canActivate()返回值

返回值包含:boolean、Promise<boolean>、Observable<boolean>(不常用),可以看出守卫既可以同步检查,也可以异步检查。并且 canActivate 返回结果决定着是否继续处理请求,返回 true 继续,返回 false NestJS会拒绝请求,并在底层抛出 ForbiddenException ,通常返回:

{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

如果需要返回其他错误,例如未登录时的 401,不要只返回 false,应主动抛出异常:

throw new UnauthorizedException("请先登录");

守卫可以对请求数据并进行修改,后续的Pipe、Controller接收到的数据是修改后的。不过建议只做它职责之内的处理,如只添加认证、授权相关的上下文信息,不要修改 body、query 或 params,这样按单一职责便于后期维护。

作用域

跟管道一样,存在方法级、Controller级、全局的守卫

方法级守卫

只作用于绑定的路由

@Controller("users")
export class UsersController {
  @Get("profile")
  // 作用于getProfile路由
  @UseGuards(AuthGuard)
  getProfile() {
    return "个人资料";
  }
}

Controller级守卫

只作用于该控制器的路由

@Controller("users")
// 作用UsersController的所有路由
@UseGuards(AuthGuard)
export class UsersController {
  @Get()
  findAll() {}

  @Get(":id")
  findOne() {}

  @Post()
  create() {}
}

全局守卫

作用于所有控制器和路由。也可通过在任意模块中使用 APP_GUARD + useClass 进行全局注册。

main.ts

const app = await NestFactory.create(AppModule);

// 注册全局守卫
app.useGlobalGuards(new AuthGuard());

UseGuards也可以传入实例@UseGuards(new AuthGuard()),但是推荐用类。app.useGlobalGuards只接受实例

存在多个守卫时,会依次执行,只要其中一个出现:返回false/抛出异常/promise被拒绝/observable抛出错误,那么后续的守卫和Controller就不会执行

常见用例

角色权限守卫

通过守卫设置角色权限

  1. 定义角色
export enum Role {
  User = "user",
  Admin = "admin",
}
  1. 创建角色元数据装饰器@Roles()
import { Reflector } from "@nestjs/core";
import { Role } from "./role.enum";

export const Roles = Reflector.createDecorator<Role[]>();
  1. 创建RolesGuard
import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Injectable,
} from "@nestjs/common";

import { Reflector } from "@nestjs/core";
import { Role } from "./role.enum";

interface CurrentUser {
  id: number;
  username: string;
  roles: Role[];
}

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(Roles, [
      context.getHandler(),
      context.getClass(),
    ]);

    // 角色列表为空,说明当前路由没有声明角色要求,默认允许
    if (!requiredRoles?.length) {
      return true;
    }

    const request = context.switchToHttp().getRequest();

    const user = request.user as CurrentUser | undefined;

    // 没有用户信息
    if (!user) {
      return false;
    }

    const hasRole = requiredRoles.some((requiredRole) =>
      user.roles.includes(requiredRole),
    );

    // 用户信息不符合角色权限
    if (!hasRole) {
      throw new ForbiddenException("当前账号没有访问权限");
    }

    return true;
  }
}
  1. 应用守卫
@Controller("users")
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
  @Get()
  @Roles(Role.User, Role.Admin)
  findAll() {
    return "用户列表";
  }

  @Post()
  @Roles(Role.Admin)
  create() {
    return "创建用户";
  }

  @Delete(":id")
  @Roles(Role.Admin)
  remove() {
    return "删除用户";
  }
}