介绍
守卫是一个用 @Injectable()
装饰器注释的类,它实现了 CanActivate
接口。
守卫有单一的责任。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。这通常称为授权。授权(及其通常与之合作的身份验证)通常由传统 Express
应用中的 中间件 处理。中间件是身份验证的不错选择,因为诸如令牌验证和将属性附加到 request
对象之类的事情与特定路由上下文(及其元数据)没有紧密联系。
但是中间件,就其本质而言,是愚蠢的。它不知道调用 next()
函数后将执行哪个处理程序。另一方面,Guards 可以访问 ExecutionContext
实例,因此确切地知道接下来要执行什么。它们的设计与异常过滤器、管道和拦截器非常相似,可让你在请求/响应周期的正确位置插入处理逻辑,并以声明方式进行。这有助于使你的代码保持干爽和声明式。
为了让程序更加智能,这里使用装饰器功能,例如在某一个请求接口上添加一个装饰器,限制重复请求等等, 接下来以俩个案例:
重复请求限制
常量文件
为什么要使用常量文件?好处就是统一管理;首先创建文件作为常量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export const LOG_KEY_METADATA = "__log__";
export const REPEAT_SUBMIT_KEY_METADATA = "__repeat_submit__";
export const PUBLIC_KEY_METADATA = "__public__";
export const PERMISSION_KEY_METADATA = "__permission__";
export const REDIS_REPEAT_SUBMIT_KEY = "__redis_repeat__:";
export const REDIS_CAPTCHA_KEY = "__captcha__:";
export const REDIS_TOKEN_KEY = "__token__:";
export const REDIS_USER_INFO_KEY = "__user_info__:";
|
自定义元数据
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { SetMetadata } from "@nestjs/common"; import { REPEAT_SUBMIT_KEY_METADATA } from "../constants/metadata.constant";
export class RepeatSubmitOption { interval?: number = 1; message?: string = "操作过于频繁"; } export const RepeatSubmit = (option?: RepeatSubmitOption) => { return SetMetadata( REPEAT_SUBMIT_KEY_METADATA, Object.assign(new RepeatSubmitOption(), option) ); };
|
自定义守卫
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { RepeatSubmitOption } from "../decorator/repeat-submit.decorator"; import { REDIS_REPEAT_SUBMIT_KEY, REPEAT_SUBMIT_KEY_METADATA, } from "../constants/metadata.constant"; import { ApiException } from "../exception/api.exception"; import { InjectRedis } from "@nestjs-modules/ioredis"; import Redis from "ioredis"; import { Request } from "express"; import { CommonService } from "src/shared/common/common.service";
@Injectable() export class RepeatSubmitGuard implements CanActivate { constructor( private readonly reflector: Reflector, @InjectRedis() private readonly redis: Redis, private readonly commonService: CommonService ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const repeatSubmitOption: RepeatSubmitOption = this.reflector.get( REPEAT_SUBMIT_KEY_METADATA, context.getHandler() ); if (!repeatSubmitOption) return true; const request: Request = context.switchToHttp().getRequest(); const _key = `${REDIS_REPEAT_SUBMIT_KEY}${request.url}`; const cache = await this.redis.get(_key); const data = { ip: this.commonService.getIp(request), body: request.body, prams: request.params, query: request.query, }; const dataStr = JSON.stringify(data); if (!cache) { if (dataStr) { this.redis.set(_key, dataStr, "EX", repeatSubmitOption.interval); } } else { if (dataStr && dataStr === cache) { throw new ApiException(repeatSubmitOption.message); } } return true; } }
|
注册
app.module.ts
使用:
1 2 3 4 5 6 7 8 9
| @Module({ imports: [], providers: [ { provide: APP_GUARD, useClass: RepeatSubmitGuard, }, ] })
|
控制层使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { Controller, Post, Body, Get, Request } from "@nestjs/common"; export class UserController { constructor() {}
@RepeatSubmit({ interval: 2, message: "操作过于频繁", }) @Post("create") async create(@Body() body: { username: string; password: string }) { return "success"; } }
|
操作权限
自定义元数据
1 2 3 4
| export enum PermissionRelationEnum { AND = "AND", OR = "OR", }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { SetMetadata } from "@nestjs/common"; import { PermissionRelationEnum } from "../enum/permission.enum"; import { PERMISSION_KEY_METADATA } from "../constants/metadata.constant";
export class PermissionOption { permissionArr: string[]; relation: PermissionRelationEnum; } export const Permission = ( permissions: string | string[], relation: PermissionRelationEnum = PermissionRelationEnum.OR ) => { const permissionObj: PermissionOption = { permissionArr: [], relation, }; if (typeof permissions === "string") { permissionObj.permissionArr = [permissions]; } else if (permissions instanceof Array) { permissionObj.permissionArr = permissions; } return SetMetadata(PERMISSION_KEY_METADATA, permissionObj); };
|
自定义守卫
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { Observable } from "rxjs"; import { PermissionOption } from "../decorator/permission.decorator"; import { PERMISSION_KEY_METADATA } from "../constants/metadata.constant"; import { PermissionRelationEnum } from "../enum/permission.enum"; import { ApiException } from "../exception/api.exception"; import { HttpStatusCode } from "../enum/http.enum";
@Injectable() export class PermissionGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} canActivate( context: ExecutionContext ): boolean | Promise<boolean> | Observable<boolean> { const permissionObj = this.reflector.getAllAndOverride<PermissionOption>( PERMISSION_KEY_METADATA, [context.getHandler(), context.getClass()] ); if (!permissionObj || !permissionObj.permissionArr.length) return true;
const request = context.switchToHttp().getRequest(); const user = request.user; const permissions = user?.permissions || []; if (permissions.includes("*")) return true; let result = false; if (permissionObj.relation === PermissionRelationEnum.OR) { result = permissionObj.permissionArr.some(userPermission => { return permissions.includes(userPermission); }); } else if (permissionObj.relation === PermissionRelationEnum.AND) { result = permissionObj.permissionArr.every(userPermission => { return permissions.includes(userPermission); }); } if (!result) throw new ApiException( "暂无权限访问,请联系管理员", HttpStatusCode.UNAUTHORIZED ); return result; } }
|
注册
app.module.ts
使用:
1 2 3 4 5 6 7 8 9 10
| @Module({ imports: [], providers: [ { provide: APP_GUARD, useClass: PermissionGuard, }, ] })
|
控制层使用
1 2 3 4 5 6 7 8 9 10 11
| import { Controller, Post, Body, Get, Request } from "@nestjs/common"; export class UserController { constructor() {}
@Permission(["v1:order:create"], PermissionRelationEnum.OR) @Post("create") async create(@Body() body: { username: string; password: string }) { return "success"; } }
|
是否登录
自定义元数据
1 2 3 4
| import { SetMetadata } from "@nestjs/common"; import { PUBLIC_KEY_METADATA } from "../constants/metadata.constant";
export const Public = () => SetMetadata(PUBLIC_KEY_METADATA, true);
|
自定义守卫
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 29 30 31 32
| import { ExecutionContext, Injectable } from "@nestjs/common"; import { Observable } from "rxjs"; import { AuthGuard } from "@nestjs/passport"; import { Reflector } from "@nestjs/core"; import { PUBLIC_KEY_METADATA } from "../constants/metadata.constant"; import { ApiException } from "../exception/api.exception"; import { HttpStatusCode } from "../enum/http.enum";
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") { constructor(private reflector: Reflector) { super(); } canActivate( context: ExecutionContext ): boolean | Promise<boolean> | Observable<boolean> { const noInterception = this.reflector.getAllAndOverride( PUBLIC_KEY_METADATA, [context.getHandler(), context.getClass()] ); if (noInterception) return true; return super.canActivate(context); }
handleRequest(err: any, user: any) { if (err || !user) { throw err || new ApiException("登录状态已过期", HttpStatusCode.FORBIDDEN); } return user; } }
|
注册
app.module.ts
使用:
1 2 3 4 5 6 7 8 9 10
| @Module({ imports: [], providers: [ { provide: APP_GUARD, useClass: JwtAuthGuard, }, ] })
|
控制层使用
1 2 3 4 5 6 7 8 9 10 11
| import { Controller, Post, Body, Get, Request } from "@nestjs/common"; export class UserController { constructor() {}
@Public() @Post("create") async create(@Body() body: { username: string; password: string }) { return "success"; } }
|
注意:
ApiException是什么可以查看 异常过滤器
Redis使用
JWT使用