NestJS简易权限守卫

216 阅读4分钟

NestJS权限守卫

1. 简要介绍

权限认证作为大多数项目的常见且重要的部分,在NestJS中也不例外。使用NestJS构建服务,可以选择通过多种方式构建权限系统

  1. 自定义全局中间件实现
  2. 通过重构NestJScanActivate函数实现

今天,就会通过第二种方式来实现在NestJS中快速构建鉴权系统(基于JWT的)

2. 项目结构与Auth模块

2.1 创建Auth模块与对应Service

 # nest g module auth
 # nest g controller auth
 # nest g service auth
 ​
 # 再创建一个auth.guard.ts来自定义保卫器
 ​
 nest开发的基本操作了

2.2 项目如下所示:

Auth模块根据项目需求可以放在server平级,也可以放在server里面

image-20240422170207042

3 构建登录模块

本质就是实现校验用户密码并返回token的过程

3.1 auth.module.ts配置

 import { Module } from '@nestjs/common';
 import { JwtModule } from '@nestjs/jwt';
 import { AuthController } from './auth.controller';
 import { AuthService } from './auth.service';
 import { APP_GUARD } from '@nestjs/core';
 import { AuthGuard } from './auth.guard';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { User } from 'src/entity/user.entity';
 ​
 @Module({
   imports: [
     JwtModule.register({
       global: true,
       secret: 'rankmobile',
       signOptions: { expiresIn: '7d' },
     }),
     TypeOrmModule.forFeature([User]),
   ],
   controllers: [AuthController],
   providers: [
     AuthService,
     {
       provide: APP_GUARD,
       useClass: AuthGuard,
     },
   ],
 })
 export class AuthModule {}
 ​

3.2 auth.service.ts配置

 import { Injectable } from '@nestjs/common';
 import { JwtService } from '@nestjs/jwt';
 import { InjectRepository } from '@nestjs/typeorm';
 import { UserLoginDto } from 'src/dto/user.dto';
 import { User } from 'src/entity/user.entity';
 import { Repository } from 'typeorm';
 @Injectable()
 export class AuthService {
   constructor(
     private jwtService: JwtService,
     @InjectRepository(User)
     private usersRepository: Repository<User>,
   ) {}
 ​
   async signIn(payload: UserLoginDto): Promise<any> {
     let token;
     const res = await this.usersRepository.findOne({
       where: { username: payload.username, password: payload.password },
     });
     if (res != null) {
       // 信息正确,返回正确的token
       token = await this.jwtService.signAsync(payload);
       console.log('Token生成');
     } else {
       // 信息正确,返回空的token,控制层再判断
       token = '';
     }
     return token;
   }
 }
 ​

3.3 auth.controller.ts配置

 import { Controller, Post, Body } from '@nestjs/common';
 import { AuthService } from './auth.service';
 import {
   ApiBody,
   ApiExtraModels,
   ApiOperation,
   ApiTags,
 } from '@nestjs/swagger';
 import { White_Router } from 'src/util/commonDecoration';
 import { UserLoginDto } from 'src/dto/user.dto';
 import { ResultData } from 'src/util/commonResponse';
 import { ApiResult } from 'src/util/commonResult';
 ​
 @Controller('auth')
 @ApiTags('权限认证模块')
 @ApiExtraModels(ResultData)
 export class AuthController {
   constructor(private authService: AuthService) {}
 ​
   @White_Router()
   @ApiOperation({ summary: '登录接口(获取token)' })
   @ApiBody({ type: UserLoginDto })
   @ApiResult(String, false, false)
   @Post('login')
   async signIn(@Body() payload: UserLoginDto) {
     const token: string = await this.authService.signIn(payload);
 ​
     if (token != '') {
       return ResultData.successResponse('ok', token);//将token返回,实现登录并返回token的过程
     } else {
       return ResultData.errorResponse(401, '无权限', '无权限');
     }
   }
 }
 ​

到此为止,即实现了鉴权模块的登陆端口部分,使得前端可以获取到token信息,最后在完成其余接口的鉴权保护(token校验)即可完成

4.构建自定义及安全保护

4.1 构建白名单装饰器

白名单问题上,使用反射机制来创建一个白名单装饰器,配置在无需校验的接口上

 //utils文件夹下commonDecoration.ts文件的代码
 //之后的白名单跨越鉴权会引用到这里的
 import { SetMetadata } from '@nestjs/common';
 ​
 export const IS_PUBLIC_KEY = 'isPublic';
 export const White_Router = () => SetMetadata(IS_PUBLIC_KEY, true);
 ​

4.2 Auth.guard.ts配置

整体上流程分为四步走:

  1. 反射实现白名单跨越鉴权
  2. 获取请求对象,拿出token
  3. token鉴权
  4. 收尾处理(成功后,请求对象挂载用户信息,失败后,抛出异常再统一处理)

下面的代码每一步都有相应的解释,大概可以看出来了

 import {
   CanActivate,
   ExecutionContext,
   Injectable,
   UnauthorizedException,
 } from '@nestjs/common';
 import { Reflector } from '@nestjs/core';
 import { JwtService } from '@nestjs/jwt';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Request } from 'express';
 import { User } from 'src/entity/user.entity';
 import { IS_PUBLIC_KEY } from 'src/util/commonDecoration';
 import { Repository } from 'typeorm';
 ​
 @Injectable()
 export class AuthGuard implements CanActivate {
   constructor(
     private jwtService: JwtService,
     private reflector: Reflector,
     @InjectRepository(User)
     private usersRepository: Repository<User>,
   ) {}
 ​
   async canActivate(context: ExecutionContext): Promise<boolean> {
     // 第一步:反射实现白名单跨越鉴权
     const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
       context.getHandler(),
       context.getClass(),
     ]);
     if (isPublic) {
       //遇到白名单接口,直接允许,进入下一步
       return true;
     }
     // 第二步:获取请求头对象,拿出token
     const request = context.switchToHttp().getRequest();
     const token = this.extractTokenFromHeader(request);
     if (!token) {
       throw new UnauthorizedException();
     }
     try {
       // 第三步:token鉴权
       const payload = await this.jwtService.verifyAsync(token, {
         secret: 'rankmobile',
       });
       const username = payload.username;
 ​
       const res = await this.usersRepository.findOne({
         where: { username: username },
       });
 ​
       // 第四步:收尾处理(成功后,请求对象挂载用户信息,失败后,抛出异常再统一处理)
       if (res) {
         request['userInfo'] = res;
       } else {
         throw new Error('无权限');
       }
     } catch {
       throw new UnauthorizedException();
     }
     return true;
   }
 ​
   private extractTokenFromHeader(request: Request): string | undefined {
     const [type, token] = request.headers.authorization?.split(' ') ?? [];
     return type === 'Bearer' ? token : undefined;
   }
 }
 ​

5. 接口绑定鉴权配置

5.1 鉴权接口绑定

   @UseGuards(AuthGuard)//配置鉴权组件
   @Get('profile')
   getUserInfo(@Request() req) {
       // --------------
       //----------------
       //  业务代码
   }

5.2 全局鉴权绑定+白名单配置

Auth.module.ts文件书写如下代码即可完成全局配置:

 import { APP_GUARD } from '@nestjs/core';
 import { AuthGuard } from './auth.guard';
 ​
 providers: [
     AuthService,
     {
       provide: APP_GUARD,
       useClass: AuthGuard,
     },
   ],

整个项目总会有几个接口不需要鉴权,加上之前配置的白名单装饰器即可了。

   @White_Router()  //配置上白名单装饰器,即可实现该接口无需鉴权
   @ApiOperation({ summary: '登录接口(获取token)' })
   @ApiBody({ type: UserLoginDto })
   @ApiResult(String, false, false)
   @Post('login')
   async signIn(@Body() payload: UserLoginDto) {
     const token: string = await this.authService.signIn(payload);
 ​
     if (token != '') {
       return ResultData.successResponse('ok', token);
     } else {
       return ResultData.errorResponse(401, '无权限', '无权限');
     }
   }
 }
 ​

本实践没有过多的文本描述,多在代码中的注释。但通过此个实践了解学习之后,应该可以较好的掌握在的React Hooks项目中应用Router V6封装整个项目的路由系统,能够真正实现一次封装,多处收益

相关的配套实践Demo会上传Github开源