Nestjs的守卫和拦截器

这一节本来是想说守卫和权限(RBAC)的,但是我们还没做登录和验证(JWT),直接说权限感觉顺序有点错乱了,但是验证这里呢又用到了守卫,所以这一节先说一下守卫和拦截器,他们放到一起因为都用到了同一个上下文ExecutionContext。下一节我们在说一下上下文的相关内容,然后再讲JWT和RBAC吧。

守卫

守卫是一个使用 @Injectable() 装饰器的类。 守卫应该实现 CanActivate 接口。

守卫是用来根据运行时出现的某些条件来确定给定的请求是否由路由处理程序处理

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

CanActivate接口定义:

1
2
3
4
5
6
7
8
9
10
11
export interface CanActivate {
/**
* @param context Current execution context. Provides access to details about
* the current request pipeline.
*
* @returns Value indicating whether or not the current request is allowed to
* proceed.
*/
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>;
}

必须实现一个canActivate方法,接收一个上下文,然后同步或异步的返回一个布尔值。如果为true则路由处理程序会处理用户调用,否则将忽略当前请求。

ExecutionContext是ArgumentHost的扩展,在这里不展开讲。

因为咱们还没有做登录鉴权,所以咱们通过模拟数据来演示一下守卫的用法

先创建roles.guard.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// roles.guard.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()
// 从请求头中获取role属性
const user = request.headers.role
// 判断是否符合我们要求的角色
return roles.includes(user)
}
}

将守卫绑定到控制器phones.controller.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// phones.controller.ts
import {
Controller,
Get,
Post,
Body,
UseGuards,
SetMetadata,
} from '@nestjs/common'
import { PhonesService } from './phones.service'
import { CreatePhoneDto } from './dto/create-phone.dto'
import { RolesGuard } from './roles.guard'

@Controller('phones')
@UseGuards(RolesGuard) // 绑定守卫
export class PhonesController {
constructor(private readonly phonesService: PhonesService) {}

@Post('create')
@SetMetadata('roles', ['admin']) // 设置路由元数据
createPhone(@Body() createPhoneDto: CreatePhoneDto) {
return 'success'
}
}

守卫与过滤器等类似,可以通过@UseGuards装饰器绑定到一个控制器或者一个处理程序,也可以通过useGlobalGuards绑定到全局。

我们在调试接口到时候往请求头塞入一个role属性

1
2
3
4
// localhost:3000/nest/phones/create
"headers": {
"role": "root"
}

接收到的响应为:

1
2
3
4
5
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}

我们将role设置为admin再次尝试:

1
2
3
4
// localhost:3000/nest/phones/create
"headers": {
"role": "admin"
}

响应为:

1
success

这样一来,我们能大概知道守卫的作用与用法了,当然真是的授权守卫要麻烦的多,我们放到后边RBAC的部分再说。

拦截器

拦截器是使用 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。可以实现:

  • 函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据所选条件完全重写函数 (例如, 缓存目的)
1
2
3
4
5
6
7
8
9
10
11
export interface NestInterceptor<T = any, R = any> {
/**
* Method to implement a custom interceptor.
*
* @param context an `ExecutionContext` object providing methods to access the
* route handler and class about to be invoked.
* @param next a reference to the `CallHandler`, which provides access to an
* `Observable` representing the response stream from the route handler.
*/
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>;
}

自定义拦截器

我们创建一个response.interceptor.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// response.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('路由处理程序运行之前')
return next.handle().pipe(tap(() => console.log(`路由处理程序运行之后`)))
}
}

目前只是具备打印功能,让我们以此来看一下执行流程

pipe(), tap()这些操作符需要查阅rxjs来学习哦。

phones.controller.ts中绑定拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// import {
Controller,
Get,
Post,
Body,
UseInterceptors,
} from '@nestjs/common'
import { PhonesService } from './phones.service'
import { CreatePhoneDto } from './dto/create-phone.dto'
import { ResponseInterceptor } from './res.interceptor'

@Controller('phones')
export class PhonesController {
constructor(private readonly phonesService: PhonesService) {}

@Post('create')
@UseInterceptors(ResponseInterceptor)
createPhone(@Body() createPhoneDto: CreatePhoneDto) {
console.log('路由处理程序运行')
return 'success'
}
}

此时我们调用接口localhost:3000/nest/phones/create,查看控制台打印内容:

1
2
3
路由处理程序运行之前
路由处理程序运行
路由处理程序运行之后

至此,拦截器的执行顺序应该能理解了。

在处理函数执行前后添加额外操作

我们对拦截器进行改造,将传入数据的body中的name进行修改,并对响应内容进行了格式化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// response.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 拿到请求体进行修改
const request = context.switchToHttp().getRequest()
const reqBody = request.body
reqBody.name = 'named by interceptor'
return next.handle().pipe(
// 将响应内容格式化
map(data => ({
success: true,
code: 200,
data,
})),
)
}
}

phones.controller.ts中也进行一些修改,直接将接收到的数据返回:

1
2
3
4
5
6
// phones.controller.ts
@Post('create')
@UseInterceptors(ResponseInterceptor)
createPhone(@Body() createPhoneDto: CreatePhoneDto) {
return createPhoneDto
}

此时我们访问接口并传入数据:

1
2
3
4
5
6
// localhost:3000/nest/phones/create
{
"name": "Iphone14",
"brand": "Apple",
"ram": 4
}

响应结果为:

1
2
3
4
5
6
7
8
9
{
"success": true,
"code": 200,
"data": {
"name": "named by interceptor",
"brand": "Apple",
"ram": 4
}
}

到这里大家应该知道怎么通过拦截器来统一格式化返回内容了。

拦截器也一样,有控制器范围、方法范围和全局范围生效的。

map()方法也是rxjs内容,需要自行学习哦,本人暂时对于rxjs没有太多接触,不敢乱说。如果后续对rxjs的学习有一些新的也会进行分享。

作者

胡兆磊

发布于

2023-02-24

更新于

2023-02-24

许可协议