在客户端发送的请求到达服务端并被路由处理器接收之前,还会调用一些特殊的类或函数,比如守卫、中间件、拦截器、管道等。本文将先介绍其中的中间件以及守卫的相关内容。
一、Middleware
中间件的作用,有过 Express 开发经验的人应该不陌生。以下的描述摘自 Express 官方文档
- 执行任何代码
- 修改 request 和 response 对象
- 结束 request-response 周期
- 调用堆栈中的下一个 next() 中间件函数
- 如果当前的中间件函数并没有结束 request-response 周期,它必须调用 next 方法将控制权移交给下一个中间件的 next()。否则,这次请求将被搁置
(一)Custom Middleware
使用 @Injectable()
装饰器让中间件可注入
实现 NestMiddleware
以及 use()
方法搭建中间件的基本结构
ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class CryptoMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
(二)Applying Middleware
按官网的描述,中间件不能像 Services、Controllers 一样通过添加到 @Module 的某一个属性中实现依赖注入。取而代之的是,我们需要执行 NestModule 接口并调用内部的 configure 方法来注入中间件并约束其应用范围。
在下面的例子中,我们将预先定义的 CryptoMiddleware 应用到 AppModule 中,并限制该中间件应用于指定路由处理器以外的所有路由。
ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { CryptoMiddleware } from '@/middlewares'
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(CryptoMiddleware)
.exclude(
{ path: 'user/signup', method: RequestMethod.POST },
{ path: 'user/login', method: RequestMethod.POST },
{ path: 'static', method: RequestMethod.ALL }
)
.forRoutes({
path: '*',
method: RequestMethod.ALL
})
}
}
为了更加灵活地约束中间件,也可以使用通配符来匹配路由:
ts
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
(三)Multiple Middleware
ts
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
(四)Global Middleware
ts
const app = await NestFactory.create(AppModule);
app.use(LoggerMiddleware);
await app.listen(3000);
二、Guard
守卫只有一个职责。它们使用运行时存在的某些条件(如权限、角色等)决定客户端的请求是否由路由处理程序处理,也就是"授权"。
授权(以及通常与之协作的同类身份验证)在 Express 应用程序中多由中间件处理。当然,中间件是一个很好的身份验证可选方案,因为像令牌验证或者将属性附加到请求对象这样的需求与特定的路由上下文并没有强关联。
但是中间件存在一个问题,它并不知道调用 next() 函数后具体将执行哪个处理程序,以RBAC【Role Based Access Control:基于角色的访问控制】为例,每个路由处理器可能要求特定的角色才能访问,我们需要对比发送请求的角色 与处理器要求的角色是否匹配来判断是否放行。
守卫可以访问 ExecutionContext 实例,因此能够确切地知道接下来要执行什么。
如下所示,假定有一个经过身份验证的用户(中间件校验用户通过并在请求头中附加了一个令牌)。AuthGuard 将提取并验证令牌,并使用提取的信息来确定请求是否可以继续。
ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class RolesGuard implements CanActivate {
// 如果返回 true,此次请求会被继续处理
// 如果返回 false,Nest 会拒绝此次请求
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
我们看到 canActivate() 携带了一个参数 context: ExecutionContext ,其中提供了多个辅助方法来提供更多关于当前执行上下文的信息,可以帮助我们编写出更加通用的守卫。接下来我们需要介绍下如何建立守卫 和处理器元数据之间的联系:
(一)绑定守卫
使用 @UseGuards 装饰器绑定守卫,根据绑定的位置和方法分为 全局守卫、控制器守卫以及方法守卫。如下所示:
ts
// global scoped
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
// controller scoped
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
// method scoped
@Controller('cats')
export class CatsController {
@UseGuards(RolesGuard)
createCat(){}
}
需要注意的是,上述的全局守卫是使用 useGlobalGuards 绑定,由于是在模块外注册,它并不能借助 Nest 的 DI 机制实现依赖注入。为了解决这一问题,可以按照下面的方法在模块内注册全局守卫。
ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
}
]
})
export class AppModule {}
虽然 RolesGuard 已经开始工作,但目前它还不知道每个处理程序允许使用哪些角色,例如,CatsController 可以对不同的路由使用不同的权限方案,有些可能只对管理员用户可用,而其他路由可能对所有人开放。为了能够以灵活且可重用的方式将角色与路由匹配,我们需要利用最重要的守卫特性------执行上下文。
第二步:为处理器函数设置角色
Nest提供了两种方法将自定义元数据附加到路由的能力,一种是使用 Reflector#createDecorator
静态方法创建角色装饰器
ts
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
ts
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
另一种是使用内置的 @SetMetadata()
来绑定角色
ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
或者进一步封装:
ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
看上去使用 Reflector#createDecorator
静态方法创建的结果差不多,但是 @SetMetadata 可以让你对元数据键和值有更多的控制,并且还创建带有多个参数的装饰器。
第三步:将 ExecutionContext 和 Reflector 集成到 RolesGuard 类
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);
}
}