Guard 具有单一职责。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否由路由处理程序处理。这通常称为授权。授权(及其通常与之协作的身份验证)通常由传统 Express 应用程序中的 中间件 处理。中间件是身份验证的不错选择,因为诸如令牌验证和将属性附加到 request
对象之类的事情与特定路由上下文(及其元数据)没有紧密联系。
但中间件本质上是愚蠢的。它不知道在调用 next()
函数后将执行哪个处理程序。另一方面,Guard 可以访问 ExecutionContext
实例,因此确切知道接下来要执行什么。它们的设计与异常过滤器、管道和拦截器非常相似,可让您在请求/响应周期的正确位置插入处理逻辑,并以声明方式进行。这有助于保持代码简洁和声明性。
Guard 在所有中间件之后执行,但在任何拦截器或管道之前执行。
授权守卫
如上所述,授权 是 Guard 的一个很好的用例,因为只有当调用者(通常是特定的经过身份验证的用户)具有足够的权限时,特定路由才可用。我们现在将构建的 AuthGuard
假设一个经过身份验证的用户(因此,令牌附加到请求标头)。它将提取并验证令牌,并使用提取的信息来确定请求是否可以继续。
// auth.guard
import { CanActivate, ExecutionContext, Injectable } 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)
}
}
validateRequest()
函数中的逻辑可以根据需要简单或复杂。此示例的主要目的是展示守卫如何融入请求/响应周期。
每个守卫都必须实现一个 canActivate()
函数。此函数应返回一个布尔值,指示当前请求是否被允许。它可以同步或异步返回响应(通过 Promise
或 Observable
)。Nest 使用返回值来控制下一个操作:
- 如果返回
true
,则将处理请求。 - 如果返回
false
,Nest 将拒绝该请求。
执行上下文
canActivate()
函数接受一个参数,即 ExecutionContext
实例。ExecutionContext
继承自 ArgumentsHost
。我们之前在异常过滤器章节中看到过 ArgumentsHost
。在上面的示例中,我们只是使用之前在 ArgumentsHost
上定义的相同辅助方法来获取对 Request
对象的引用。您可以参考 异常过滤器 章节的 Arguments host 部分,了解有关此主题的更多信息。
通过扩展 ArgumentsHost
,ExecutionContext
还添加了几个新的辅助方法,这些方法提供有关当前执行过程的更多详细信息。这些详细信息有助于构建更通用的保护程序,这些保护程序可以跨广泛的控制器、方法和执行上下文工作。在此处 了解有关 ExecutionContext
的更多信息。
基于角色的身份验证
让我们构建一个功能更强大的防护器,仅允许具有特定角色的用户访问。我们将从一个基本的防护器模板开始,并在接下来的部分中对其进行构建。目前,它允许所有请求继续进行:
// roles.guard
import { CanActivate, ExecutionContext, Injectable } 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 {}
@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())
在混合应用的情况下,useGlobalGuards()
方法默认不会为网关和微服务设置保护(有关如何更改此行为的信息,请参阅混合应用)。对于标准
(非混合)微服务应用,useGlobalGuards()
会全局安装保护。
全局保护用于整个应用,适用于每个控制器和每个路由处理程序。就依赖注入而言,从任何模块外部注册的全局保护(使用上述示例中的 useGlobalGuards()
)无法注入依赖项,因为这在任何模块的上下文之外完成。为了解决这个问题,您可以使用以下构造直接从任何模块设置保护:
import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
使用此方法为保护器执行依赖注入时,请注意,无论使用此构造的模块是什么,保护器实际上都是全局的。应该在哪里执行此操作?选择定义保护器(上例中的RolesGuard
)的模块。此外,useClass
不是处理自定义提供程序注册的唯一方法。了解更多信息 此处。
设置每个处理程序的角色
我们的RolesGuard
正在运行,但它还不是很智能。我们还没有利用最重要的保护功能 - 执行上下文。它还不知道角色,也不知道每个处理程序允许哪些角色。例如,CatsController
可以为不同的路由提供不同的权限方案。有些可能只对管理员用户可用,而其他可能对所有人开放。我们如何以灵活且可重复使用的方式将角色与路由匹配?
这就是自定义元数据发挥作用的地方(了解更多此处(https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata))。 Nest 提供了通过Reflector#createDecorator
静态方法创建的装饰器或内置的@SetMetadata()
装饰器将自定义元数据附加到路由处理程序的功能。
例如,让我们使用Reflector#createDecorator
方法创建一个@Roles()
装饰器,它将元数据附加到处理程序。框架开箱即用的Reflector
并从@nestjs/core
包中公开。
import { Reflector } from '@nestjs/core'
export const Roles = Reflector.createDecorator<string[]>()
此处的Roles
装饰器是一个接受单个string[]
类型参数的函数。
现在,要使用此装饰器,我们只需用它来注释处理程序:
// cats.controller
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
在这里,我们将 Roles
装饰器元数据附加到 create()
方法,表示只有具有 admin
角色的用户才应被允许访问此路由。
或者,我们可以使用内置的 @SetMetadata()
装饰器,而不是使用 Reflector#createDecorator
方法。详细了解 此处。
将它们放在一起
现在让我们回过头来将它与我们的 RolesGuard
结合在一起。目前,它在所有情况下都简单地返回 true
,允许每个请求继续进行。我们希望根据将 分配给当前用户 的角色与当前正在处理的路由所需的实际角色进行比较,使返回值具有条件性。为了访问路由的角色(自定义元数据),我们将再次使用 Reflector
辅助类,如下所示:
// roles.guard
import { CanActivate, ExecutionContext, Injectable } 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)
}
}
在 node.js 世界中,将授权用户附加到 request
对象是一种常见做法。因此,在上面的示例代码中,我们假设 request.user
包含用户实例和允许的角色。在您的应用中,您可能会在自定义 身份验证保护(或中间件)中建立该关联。查看 本章 了解有关此主题的更多信息。
matchRoles()
函数中的逻辑可以根据需要简单或复杂。此示例的主要目的是展示保护如何适应请求/响应周期。
有关以上下文敏感的方式使用反射器
的更多详细信息,请参阅执行上下文一章的反射和元数据部分。
当权限不足的用户请求端点时,Nest 会自动返回以下响应:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
请注意,在幕后,当守卫返回false
时,框架会抛出ForbiddenException
。如果您想返回不同的错误响应,则应抛出自己的特定异常。例如:
throw new UnauthorizedException()
守卫抛出的任何异常都将由异常层(全局异常过滤器和应用于当前上下文的任何异常过滤器)处理。
如果您正在寻找有关如何实现授权的真实示例,请查看本章。