Skip to main content

守卫

守卫是一个用 @Injectable() 装饰器注解的类,实现了 CanActivate 接口。

Nest.js守卫

守卫有一个单一的责任, 即根据运行时存在的特定条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。 这通常被称为授权。在传统的 Express 应用程序中, 授权(以及通常与之协作的身份验证)通常由中间件处理。 对于身份验证来说中间件是一个很好的选择, 因为像令牌验证和将属性附加到请求对象这样的事情与特定的路由上下文(及其元数据)没有强烈的关联。

但是中间件本质上是愚蠢的。 它不知道在调用 next() 函数后将执行哪个处理程序。 另一方面,守卫可以访问 ExecutionContext 实例, 因此确切地知道接下来将执行什么。 它们的设计,就像异常过滤器、管道和拦截器一样, 让您能够在请求/响应周期的确切位置插入处理逻辑, 并以声明性的方式这样做。 这有助于保持您的代码DRY和声明性。

tip

守卫在所有中间件之后执行,但在任何拦截器或管道之前执行。

授权守卫

如前所述,守卫对于授权是一个很好的用例, 因为只有在调用者(通常是特定经过身份验证的用户)具有足够权限时, 特定路由才应该可用。 我们将要构建的 AuthGuard 假设有一个经过身份验证的用户(因此请求头附加了令牌)。 它将提取和验证令牌,并使用提取的信息来确定请求是否可以继续。

auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
note

如果您正在寻找如何在应用程序中实现身份验证机制的实际示例, 请访问本章。 同样,要获取更复杂的授权示例, 请查看此页面

validateRequest() 函数内部的逻辑可以根据需要是简单或复杂的。 此示例的主要重点是展示守卫如何适应请求/响应周期。

每个守卫都必须实现一个 canActivate() 函数。 此函数应返回一个布尔值,指示当前请求是否允许。 它可以同步或异步地返回响应(通过 PromiseObservable)。 Nest 使用返回值来控制接下来的操作:

  • 如果返回 true,则将处理请求。
  • 如果返回 false,则 Nest 将拒绝请求。

执行上下文

canActivate() 函数接受一个参数,即 ExecutionContext 实例ExecutionContext 继承自 ArgumentsHost,在异常过滤器章节中我们已经见过 ArgumentsHost。 在上面的示例中,我们只是使用在 ArgumentsHost 上定义的与之前相同的辅助方法,以获取对 Request 对象的引用。 您可以回顾异常过滤器章节的 Arguments host 部分, 以了解更多关于此主题的信息。

通过扩展 ArgumentsHostExecutionContext 还添加了几个新的辅助方法, 提供有关当前执行过程的附加详细信息。 这些详细信息对于构建更通用的守卫非常有帮助, 这些守卫可以在广泛的控制器、方法和执行上下文中工作。 在这里了解有关 ExecutionContext 的更多信息。

基于角色的身份验证

让我们构建一个更实用的守卫,仅允许具有特定角色的用户访问。 我们将从一个基本的守卫模板开始,在接下来的部分中进行扩展。 目前,它允许所有请求继续进行:

roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}

绑定守卫

与管道和异常过滤器一样,守卫可以是控制器范围、方法范围或全局范围的。 在下面的示例中,我们使用 @UseGuards() 装饰器设置了一个控制器范围的守卫。 此装饰器可以接受单个参数或逗号分隔的参数列表。这使您可以通过一个声明轻松地应用适当的守卫集。

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
tip

@UseGuards() 装饰器是从 @nestjs/common 包中导入的。

在上面的示例中,我们传递了 RolesGuard 类(而不是实例),将实例化的责任留给了框架, 并启用了依赖注入。与管道和异常过滤器一样,我们也可以传递一个就地实例:

@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

上面的构造将守卫附加到此控制器声明的每个处理程序。 如果我们希望守卫仅适用于单个方法,我们在方法级别应用 @UseGuards() 装饰器。

为了设置全局守卫,请使用 Nest 应用程序实例的 useGlobalGuards() 方法:

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
tip

在混合应用程序的情况下, 默认情况下 useGlobalGuards() 方法不会为网关和微服务设置守卫(有关如何更改此行为的信息, 请参见混合应用程序)。 对于“标准”(非混合)微服务应用程序, useGlobalGuards() 确实会在全局范围内挂载守卫。

全局守卫在整个应用程序中使用,适用于每个控制器和每个路由处理程序。 在依赖注入方面,从任何模块外部注册的全局守卫(如上例中的 useGlobalGuards())不能注入依赖项, 因为这是在任何模块的上下文之外完成的。 为了解决这个问题,您可以使用以下构造直接从任何模块设置守卫:

app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

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

使用这种方法进行守卫的依赖注入时,请注意无论在哪个模块中使用此构造, 守卫实际上都是全局的。 在哪里完成这个操作? 选择定义了守卫(上例中的 `RolesGuard`)的模块。 此外,useClass 并不是处理自定义提供者注册的唯一方法。 在此处了解更多信息

为处理程序设置角色

我们的 RolesGuard 能够工作,但还不够智能。 我们尚未充分利用最重要的守卫功能 - 执行上下文。 它尚不知道角色,或者每个处理程序所需的实际角色是哪些。 例如,CatsController 可能对不同的路由采用不同的权限方案。 对于管理员用户,有些可能只对特定路由可用,而其他可能对所有人开放。 我们如何以一种灵活且可重用的方式将角色匹配到路由?

这就是自定义元数据发挥作用的地方(在这里了解更多)。 Nest 提供了通过 Reflector#createDecorator 静态方法创建的装饰器, 或者内置的 @SetMetadata() 装饰器,将自定义元数据附加到路由处理程序的能力。

例如,让我们使用 Reflector#createDecorator 方法创建一个 @Roles() 装饰器, 该装饰器将元数据附加到处理程序。

tip

Reflector 是由框架提供的,从 @nestjs/core 包中公开。

roles.decorator.ts
import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();

这里的 Roles 装饰器是一个接受类型为 string[] 的单一参数的函数。

现在,要使用此装饰器,我们只需用它注解处理程序:

cats.controller.ts
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

在这里,我们已将 Roles 装饰器元数据附加到 create() 方法, 表示只允许具有 admin 角色的用户访问此路由。

或者,我们可以使用内置的 @SetMetadata() 装饰器,了解更多信息, 请查看此处

将所有内容综合起来

现在让我们回过头来,将这一切与我们的 RolesGuard 结合在一起。 目前,它在所有情况下都只返回 true,允许每个请求继续。 我们希望根据比较当前用户被分配的角色与当前正在处理的路由所需的实际角色来使返回值有条件。 为了访问路由的角色(自定义元数据),我们将再次使用 Reflector 辅助类,如下所示:

roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';

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

canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
note

在 node.js 的世界中,将授权用户附加到请求对象是一种常见做法。 因此,在我们上面的示例代码中,我们假设 request.user 包含用户实例和允许的角色。 在您的应用程序中,您可能会在自定义身份验证守卫(或中间件)中进行这种关联。 有关此主题的更多信息,请查看此章节

warning

matchRoles() 函数内部的逻辑可以根据需要是简单或复杂的。 此示例的主要重点是展示守卫如何适应请求/响应周期。

有关在上下文敏感方式中利用 Reflector 的更多详细信息, 请参阅执行上下文章节的 反射和元数据部分

当具有不足权限的用户请求端点时,Nest 会自动返回以下响应:

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

请注意,在幕后,当守卫返回 false 时,框架会抛出 ForbiddenException。 如果您想返回不同的错误响应,应该抛出自己的特定异常。例如:

throw new UnauthorizedException();

守卫抛出的任何异常都将由异常层 (全局异常过滤器和应用于当前上下文的任何异常过滤器)处理。

note

如果您正在寻找在应用程序中如何实现授权的实际示例, 请查看此章节