Nest实战 - 认证登陆

3,211 阅读13分钟

前言

本系列主要通过实现一个后台管理系统作为切入点,帮助掘友熟悉nest的开发套路

本系列作为前后端分离项目,主要讲解nest相关知识,前端项目知识一笔带过

由于上一章的通用模板中有测试代码,我重新上传了一份干净的代码demo/v2,如下

image.png

完整的示例代码在最后

本章属于实战系列第一章,主要讲解登陆模块相关内容,包括

  • MD5加密
  • 数据库设计
  • JWT认证
  • Nest知识点(模块循环引用、自定义装饰器)
  • ts 赋值合并、交叉类型 联合类型
  • 等等

技术栈

  • node:19.x
  • 后端:nest + mysql
  • 前端:react + redux + antd

数据库设计

employee表设计如下

  • image.png

表介绍

  • id 为主键自增
  • 用户名和身份证号做了唯一约束,防止重名
  • 本系列每张表都会有创建时间、更新时间、创建人和更新人,主要为了留痕
  • 其他的字段都比较中规中矩

导入数据

  • 打开Navicat
  • image.png
  • image.png
  • 选择sql文件,在项目根目录/doc/nest-study.sql,点击开始
  • image.png
  • 出现这样的界面就可以了
  • image.png
  • 然后刷新表就可以看到数据了
  • image.png
  • 这样数据库就设计好了,接下来就可以开始开发了

登陆模块开发

新建模块

  • 命令行执行 nest g res employee
  • 第一步选择 REST API
  • 第二步选择y
  • 第一次创建模块有点慢,耐心等待即可
  • image.png
  • 出现下面的内容,模块就创建成功了
  • image.png

优化模块内容

  • 删除dto下的文件夹,如无特殊情况直接使用实体类即可
  • 删除servicecontroller 中生成的方法,保留干爽内容
  • image.png

更新实体类

  • 打开src/employee/entities/employee.entity.ts,写入
     import { ApiProperty } from '@nestjs/swagger';
     import { BaseEntity } from 'src/common/database/baseEntity';
     import { Column, Entity } from 'typeorm';
     @Entity()
     export class Employee extends BaseEntity {
       @ApiProperty({
         description: '用户姓名',
       })
       @Column({
         comment: '用户姓名',
         unique: true,
       })
       name: string;
    
       @ApiProperty({
         description: '用户生日',
       })
       @Column({
         comment: '用户生日',
       })
       birthday: Date;
    
       @ApiProperty({
         description: '用户性别 0 男 1 女',
       })
       @Column({
         comment: '用户性别 0 男 1 女',
       })
       gender: number;
    
       @ApiProperty({
         description: '身份证号码',
       })
       @Column({
         comment: '身份证号码',
         unique: true,
       })
       idNumber: string;
    
       @ApiProperty({
         description: '手机号',
       })
       @Column({
         comment: '手机号',
       })
       phone: string;
    
       @ApiProperty({
         description: '账户名称-登陆时的账号',
       })
       @Column({
         comment: '账户名称-登陆时的账号',
       })
       username: string;
    
       @ApiProperty({
         description: '账户密码',
       })
       @Column({
         comment: '账户密码',
       })
       password: string;
    
       @ApiProperty({
         description: '状态 0:禁用,1:正常',
       })
       @Column({
         comment: '状态 0:禁用,1:正常',
       })
       status: number;
    
       @ApiProperty({
         description: '头像',
       })
       @Column({
         comment: '头像',
       })
       avatar: string;
     }
    
  • 由于id createTime updateTime createUser updateUser属于公共字段,抽离成独立的文件会更好
  • 新建src/common/database/baseEntity.ts,写入
    import { ApiProperty } from '@nestjs/swagger';
    import {
      Column,
      CreateDateColumn,
      Entity,
      PrimaryGeneratedColumn,
      UpdateDateColumn,
    } from 'typeorm';
    
    /**
     * 基础实体
     */
    
    @Entity()
    export class BaseEntity {
      @ApiProperty({
        description: 'id',
      })
      @PrimaryGeneratedColumn({
        type: 'bigint',
        comment: '主键ID',
      })
      id: string;
    
      @ApiProperty({
        description: '创建时间',
      })
      @CreateDateColumn({
        comment: '创建时间',
      })
      createTime: Date;
    
      @ApiProperty({
        description: '更新时间',
      })
      @UpdateDateColumn({
        comment: '更新时间',
      })
      updateTime: Date;
    
      @ApiProperty({
        description: '创建人',
      })
      @Column({
        comment: '创建人',
      })
      createUser: Date;
    
      @ApiProperty({
        description: '更新人',
      })
      @Column({
        comment: '更新人',
      })
      updateUser: Date;
    }
    
    

关闭数据同步功能

  • 由于我们导入了表数据,需要typeORM提供的数据同步功能,不然会清空之前的数据
  • 记得把数据库密码改成自己的
  • 修改根目录/.config/.dev.yml MYSQL_CONFIG 下的synchronize:false即可
  • image.png

登陆开发

集成数据库到employee模块中

  • 修改employee.module.ts, 注入实体Employeeemployee 模块中
    import { Module } from '@nestjs/common';
    import { EmployeeService } from './employee.service';
    import { EmployeeController } from './employee.controller';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { Employee } from './entities/employee.entity';
    
    @Module({
     imports: [TypeOrmModule.forFeature([Employee])],
     controllers: [EmployeeController],
     providers: [EmployeeService],
    })
    export class EmployeeModule {}
    
  • 修改employee.service.ts,消费TypeOrmModule.forFeature注入的数据模型
    import { Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { Employee } from './entities/employee.entity';
    
    @Injectable()
    export class EmployeeService {
      @InjectRepository(Employee)
      private readonly employeeRepository: Repository<Employee>;
    }
    
  • 这样就可以在service中,使用当前实体了

开发前置-验证和序列化器

  • 介绍
    • 添加验证序列化器主要是为了对入参和返回值做校验、净化和数据转换
验证器
  • 介绍
  • 安装
    yarn add class-validator class-transformer
    
  • 修改app.module.ts添加providers即可
        {
          // 管道 - 验证
          provide: APP_PIPE,
          useFactory: () => {
            return new ValidationPipe({
              transform: true, // 属性转换
            });
          },
        }
    
序列化
  • 介绍
    • 序列化(Serialization)是一个在网络响应中返回对象前的过程。 这是一个适合转换和净化要返回给客户的数据的地方。例如,应始终从最终响应中排除敏感数据(如用户密码)。此外,某些属性可能需要额外的转换,比方说,我们只想发送一个实体的子集。手动完成这些转换既枯燥又容易出错,并且不能确定是否覆盖了所有的情况。
    • 序列化器不用额外安装npm包,nest内置了拦截器 提供支持
  • 修改app.module.ts添加providers即可
        {
          // 序列化器 - 转换和净化数据
          provide: APP_INTERCEPTOR,
          useClass: ClassSerializerInterceptor,
        },
    
  • 如下所示
  • image.png

接口开发

代码开发
  • 安装 md5, 登陆时要用
       yarn add md5
    
  • 修改employee.controller.ts
    import { Body, Controller, Post } from '@nestjs/common';
    import { EmployeeService } from './employee.service';
    import { ApiOperation, ApiTags } from '@nestjs/swagger';
    import { Employee } from './entities/employee.entity';
    import { CustomException } from 'src/common/exceptions/custom.exception';
    import * as md5 from 'md5';
    
    @ApiTags('员工模块')
    @Controller('employee')
    export class EmployeeController {
      constructor(private readonly employeeService: EmployeeService) {}
    
      @ApiOperation({
        summary: '员工登陆',
      })
      @Post('login')
      async login(@Body() employee: Employee) {
        const { username, password } = employee;
        const _employee = await this.employeeService.findByUsername(username);
    
        // 判断能否通过账号查询出用户信息
        if (!_employee) {
          // 查不到,返回用户名错误信息
          throw new CustomException('账号不存在,请重新输入');
        }
        // 判断员工是否被禁用
        if (_employee.status === 0) {
          throw new CustomException('当前员工已禁用');
        }
        // 能查到,对输入的密码进行 md5加密,对比密码,
        if (md5(password) !== _employee.password) {
          // 不一致,返回密码错误信息
          throw new CustomException('密码不对,请重新输入');
        }
        // 密码一致,返回用户信息-需要剔除密码
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { password: _password, ...rest } = _employee;
        return rest;
      }
    }
    
  • 修改employee.service.ts
        import { Injectable } from '@nestjs/common';
        import { InjectRepository } from '@nestjs/typeorm';
        import { Repository } from 'typeorm';
        import { Employee } from './entities/employee.entity';
    
        @Injectable()
        export class EmployeeService {
          @InjectRepository(Employee)
          private readonly employeeRepository: Repository<Employee>;
    
          /**
           *
           * @param username 用户名
           * @returns 根据账户名查找用户信息
           */
          findByUsername(username: Employee['username']) {
            return this.employeeRepository.findOneBy({ username });
          }
        }
    
解释
  • 密码采用md5加密对比,是因为存储的密码一般是以秘文的形式,防止密码泄漏后不法分子登陆账号
  • 返回的时候需要剔除密码字段,把密码返回给前端会给不法分子可乘之机
  • employeeService中的方法并不是login,而是findByUsername,是为了防止当前employeeService中的方法用在其他模块中,调用此方法时会引起歧义,比如,认证模块也需要通过账户名查询用户信息,调用employeeService下的login方法,有很大的心智负担
测试
  • 启动项目,打开swagger进行测试 http://localhost:3000/api

  • image.png

  • image.png

  • image.png

测试结果
  • 我们通过输入错误的账户名、错误的密码以及正确的账户名和密码,发现和我们代码中的逻辑是一样的

认证(Authentication)

介绍
  • 开发项目的过程中,尤其是B端,绝大部分接口是需要登陆后才能访问,为了防止不登陆,裸访问需要登陆的接口,故此需要对接口进行前置认证
技术 - Passport
  • nest集成了社区优秀的认证技术Passport
  • 安装
        yarn add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
        // @types/下的包主要提供类型提示
        yarn add @types/passport-local @types/passport-jwt -D
    
代码开发
  • 打开终端,利用nest 提供的命令安装 auth 模块,当然,手动创建对应的文件夹也可以,手动创建模块之后,不要忘记在app.module.ts中引入auth模块
        nest g module auth
        nest g service auth
    
  • 修改employee.module.ts,添加 exports,导出 EmployeeServiceAuth模块使用
    import { Module } from '@nestjs/common';
    import { EmployeeService } from './employee.service';
    import { EmployeeController } from './employee.controller';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { Employee } from './entities/employee.entity';
    
    @Module({
      imports: [TypeOrmModule.forFeature([Employee])],
      controllers: [EmployeeController],
      providers: [EmployeeService],
      exports: [EmployeeService],
    })
    export class EmployeeModule {}
    
  • 打开 auth.module.ts导入 EmployeeModule模块,在AuthService中会使用它
    import { Module } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { EmployeeModule } from '../employee/employee.module';
    
    @Module({
      imports: [EmployeeModule],
      providers: [AuthService],
    })
    export class AuthModule {}
    
  • 打开 auth.service.ts 添加validateEmployee方法,并调用employeeService.findByUsername,验证用户
    import { Injectable } from '@nestjs/common';
    import { EmployeeService } from '../employee/employee.service';
    import { Employee } from '../employee/entities/employee.entity';
    
    @Injectable()
    export class AuthService {
     constructor(private readonly employeeService: EmployeeService) {}
    
     /**
      *
      * @param username 用户名
      * @param pass 密码
      * @returns 验证用户
      */
     async validateEmployee(
       username: Employee['username'],
       pass: Employee['password'],
     ) {
       const employee = await this.employeeService.findByUsername(username);
       if (employee?.password === pass) {
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         const { password, ...rest } = employee;
         return rest;
       }
       return null;
     }
    }
    
  • 新建src/auth/strategy/local.strategy.ts添加Passport本地策略,进行本地身份验证
    import { Injectable } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { Strategy } from 'passport-local';
    import { AuthService } from '../auth.service';
    import { Employee } from '../../employee/entities/employee.entity';
    import { CustomException } from 'src/common/exceptions/custom.exception';
    
    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy) {
      constructor(private readonly authService: AuthService) {
        super();
      }
    
      /**
       * 本地身份验证
       * @param username 用户名
       * @param password 密码
       */
      async validate(
        username: Employee['username'],
        password: Employee['password'],
      ) {
        const employee = await this.authService.validateEmployee(
          username,
          password,
        );
        // 验证不通过,通过自定义异常类返回权限异常信息
        if (!employee) {
          throw CustomException.throwForbidden();
        }
        return employee;
      }
    }
    
  • 由于继承了PassportStrategy,需要把模块PassportModule加到nestIOC
  • 修改auth.module.ts
    import { Module } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { EmployeeModule } from '../employee/employee.module';
    import { PassportModule } from '@nestjs/passport';
    
    @Module({
      imports: [EmployeeModule, PassportModule],
      providers: [AuthService],
    })
    export class AuthModule {}
    
  • 新建 src/auth/guard/local-auth.guard.ts,进行守卫拦截
    import { Injectable } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    
    @Injectable()
    export class LocalAuthGuard extends AuthGuard('local') {}
    
  • 本地认证这样就可以了,接下来写入JWT认证,最后做🙆‍♂统一测试
  • 修改auth.service.ts,生成JWT,并添加login方法
    import { Injectable } from '@nestjs/common';
    import { JwtService } from '@nestjs/jwt';
    import { EmployeeService } from '../employee/employee.service';
    import { Employee } from '../employee/entities/employee.entity';
    
    @Injectable()
    export class AuthService {
     constructor(
       private readonly employeeService: EmployeeService,
       private readonly jwtService: JwtService,
     ) {}
    
     /**
      *
      * @param username 用户名
      * @param pass 密码
      * @returns 验证用户
      */
     async validateEmployee(
       username: Employee['username'],
       pass: Employee['password'],
     ) {
       const employee = await this.employeeService.findByUsername(username);
       if (employee?.password === pass) {
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         const { password, ...rest } = employee;
         return rest;
       }
       return null;
     }
    
     async login(employee: Employee) {
       const payload = { username: employee.username, id: employee.id };
       // 使用JWT生成token
       return {
         token: this.jwtService.sign(payload),
       };
     }
    }
    
  • 修改配置文件,加入JWT配置,打开根目录/.config/.dev.yml,写入
    HTTP:
      host: "localhost"
      port: 3000
    
    # jwt
    JWT:
      secret: secretKey
      signOptions: 
        expiresIn: 60s
    
      # mysql
    MYSQL_CONFIG:
      type: mysql # 数据库链接类型
      host: localhost
      port: 3306
      username: "root" # 数据库链接用户名
      password: "Tyf12345" # 数据库链接密码
      database: "nest-study" # 数据库名
      logging: true # 数据库打印日志
      synchronize: false # 是否开启同步数据表功能
      autoLoadEntities: true # 是否自动加载实体
    
  • 刚刚在AuthService中加入了 jwt生成 token的功能,故需要在auth.module.ts中加入 JwtModule
    import { Module } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { EmployeeModule } from '../employee/employee.module';
    import { PassportModule } from '@nestjs/passport';
    import { JwtModule } from '@nestjs/jwt';
    import { getConfig } from 'src/common/utils/ymlConfig';
    import { LocalStrategy } from './strategy/local.strategy';
    
    @Module({
      imports: [
        EmployeeModule,
        PassportModule,
        JwtModule.register({ ...getConfig('JWT') }),
      ],
      providers: [AuthService, LocalStrategy],
    })
    export class AuthModule {}
    
  • 新建src/auth/strategy/jwt.strategy.ts,写入
        import { Injectable } from '@nestjs/common';
        import { PassportStrategy } from '@nestjs/passport';
        import { ExtractJwt, Strategy } from 'passport-jwt';
        import { getConfig } from '../../common/utils/ymlConfig';
        import { Employee } from '../../employee/entities/employee.entity';
        import { TIdAndUsername } from '../../types/index';
    
        @Injectable()
        export class JwtStrategy extends PassportStrategy(Strategy) {
          constructor() {
            super({
              // 提供从请求中提取 JWT 的方法。我们将使用在 API 请求的授权头中提供token的标准方法
              jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
              //   false 将JWT没有过期的责任委托给Passport模块
              ignoreExpiration: false,
              //   密钥
              secretOrKey: getConfig('JWT')['secret'],
            });
          }
    
          //  jwt验证
          async validate(
            payload: Pick<Employee, TIdAndUsername> & { iat: number; exp: number },
          ) {
            return {
              id: payload.id,
              username: payload.username,
            };
          }
        }
    
  • 新建 src/auth/guard/jwt-auth.guard.ts, 写入
     import { ExecutionContext, Injectable } from '@nestjs/common';
    import { Reflector } from '@nestjs/core';
    import { AuthGuard } from '@nestjs/passport';
    import { Observable } from 'rxjs';
    import { IS_PUBLIC_KEY } from '../constants';
    import { CustomException } from 'src/common/exceptions/custom.exception';
    
    @Injectable()
    export class JwtAuthGuard extends AuthGuard('jwt') {
      constructor(private readonly reflector: Reflector) {
        super();
      }
      override canActivate(
        context: ExecutionContext,
      ): boolean | Promise<boolean> | Observable<boolean> {
        // 自定义认证逻辑
        const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
          context.getHandler(),
          context.getClass(),
        ]);
        if (isPublic) {
          return true;
        }
        return super.canActivate(context);
      }
    
      override handleRequest<TUser = any>(
        err: any,
        user: any,
        info: any,
        context: ExecutionContext,
        status?: any,
      ): TUser {
        if (err || !user) {
          // 可以报出一个自定义异常
          throw CustomException.throwForbidden();
        }
        return user;
      }
    }
    
  • 新建src/auth/constants.ts,写入
    import { SetMetadata } from '@nestjs/common';
    
    export const IS_PUBLIC_KEY = 'isPublic';
    
    export const isPublic = () => SetMetadata(IS_PUBLIC_KEY, true);
    
    
  • 修改 src/types/index.d.ts,合并类型到exporessrequest
    import { Request } from 'express';
    import { Employee } from '../employee/entities/employee.entity';
    declare namespace NodeJS {
      interface ProcessEnv {
        RUNNING: string;
      }
    }
    
    export type TIdAndUsername = 'id' | 'username';
    
    declare module 'express' {
      interface Request {
        user: Pick<Employee, TIdAndUsername>;
      }
    }
    
  • 接下来添加JWT守卫app.module.ts中,使其全局生效
     import {
      ClassSerializerInterceptor,
      Module,
      ValidationPipe,
    } from '@nestjs/common';
    import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
    
    import { BaseExceptionFilter } from './common/exceptions/base.exception.filter';
    import { HttpExceptionFilter } from './common/exceptions/http.exception.filter';
    import { TransformInterceptor } from './common/interceptors/transform.interceptor';
    import { ConfigModule } from '@nestjs/config';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { DataSource } from 'typeorm';
    import { getConfig } from './common/utils/ymlConfig';
    import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
    import { addTransactionalDataSource } from 'typeorm-transactional';
    import { EmployeeModule } from './employee/employee.module';
    import { AuthModule } from './auth/auth.module';
    import { JwtAuthGuard } from './auth/guard/jwt-auth.guard';
    @Module({
      imports: [
        // 配置
        ConfigModule.forRoot({
          isGlobal: true,
          ignoreEnvFile: true,
          load: [getConfig],
        }),
        // 数据库
        TypeOrmModule.forRootAsync({
          useFactory() {
            return {
              ...getConfig('MYSQL_CONFIG'),
              namingStrategy: new SnakeNamingStrategy(),
            };
          },
          async dataSourceFactory(options) {
            if (!options) {
              throw new Error('Invalid options passed');
            }
            return addTransactionalDataSource(new DataSource(options));
          },
        }),
        EmployeeModule,
        AuthModule,
      ],
      controllers: [],
      providers: [
        {
          // 管道 - 验证
          provide: APP_PIPE,
          useFactory: () => {
            return new ValidationPipe({
              transform: true, // 属性转换
            });
          },
        },
        {
          // 守卫 jwt认证
          provide: APP_GUARD,
          useClass: JwtAuthGuard,
        },
        {
          // 序列化器 - 转换和净化数据
          provide: APP_INTERCEPTOR,
          useClass: ClassSerializerInterceptor,
        },
        {
          // 全局拦截器
          provide: APP_INTERCEPTOR,
          useClass: TransformInterceptor,
        },
        {
          // 全局异常
          provide: APP_FILTER,
          useClass: BaseExceptionFilter,
        },
        {
          // Http异常
          provide: APP_FILTER,
          useClass: HttpExceptionFilter,
        },
      ],
    })
    export class AppModule {}
    
  • 接下来就该修改employee.controller.ts中的login方法,将注解@UseGuards(LocalAuthGuard)@isPublic() 挂在到 login上即可,同时为了验证认证功能,我们再创建一个test方法,通过自定义装饰器@User拿到 经过验证的JWT数据,使其返回
  • 新建src/common/decorators/user.decorator.ts,添加自定义装饰器
    // 自定义装饰器
    
    import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    import { Request } from 'express';
    import { TIdAndUsername } from 'src/types';
    import { Employee } from '../../employee/entities/employee.entity';
    
    export const User = createParamDecorator<
      TIdAndUsername,
      ExecutionContext,
      | Pick<Employee, TIdAndUsername>
      | Pick<Employee, TIdAndUsername>[TIdAndUsername]
    >((data, ctx) => {
      const user = ctx.switchToHttp().getRequest<Request>().user;
      if (data && user) {
        return user[data];
      }
      return user;
    });
    
    
  • 最后再回来修改employee.controller.ts
    import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
    import { EmployeeService } from './employee.service';
    import { ApiOperation, ApiTags } from '@nestjs/swagger';
    import { Employee } from './entities/employee.entity';
    import { CustomException } from 'src/common/exceptions/custom.exception';
    import * as md5 from 'md5';
    import { AuthService } from '../auth/auth.service';
    import { LocalAuthGuard } from 'src/auth/guard/local-auth.guard';
    import { isPublic } from 'src/auth/constants';
    import { TIdAndUsername } from '../types/index';
    import { User } from 'src/common/decorators/user.decorator';
    
    @ApiTags('员工模块')
    @Controller('employee')
    export class EmployeeController {
      constructor(
        private readonly employeeService: EmployeeService,
        private readonly authService: AuthService,
      ) {}
    
      @ApiOperation({
        summary: '员工登陆',
      })
      @isPublic()
      @UseGuards(LocalAuthGuard)
      @Post('login')
      async login(@Body() employee: Employee) {
        const { username, password } = employee;
        const _employee = await this.employeeService.findByUsername(username);
    
        // 判断能否通过账号查询出用户信息
        if (!_employee) {
          // 查不到,返回用户名错误信息
          throw new CustomException('账号不存在,请重新输入');
        }
    
        // 判断员工是否被禁用
        if (_employee.status === 0) {
          throw new CustomException('当前员工已禁用');
        }
    
        // 能查到,对输入的密码进行 md5加密,对比密码,
        if (md5(password) !== _employee.password) {
          // 不一致,返回密码错误信息
          throw new CustomException('密码不对,请重新输入');
        }
        // 密码一致,返回用户信息-需要剔除密码
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { password: _password, ...rest } = _employee;
        const tokenObj = await this.authService.login(_employee);
        return { ...rest, ...tokenObj };
      }
    
      @ApiOperation({
        summary: '测试接口认证',
      })
      @Get('/test')
      test(@User() user: Pick<Employee, TIdAndUsername>) {
        return user;
      }
    }
    
    
  • 由于在login方法中调用了AuthService中的login方法,需要导出AuthService
  • 但是直接导出,在EmployeeModule中引入,会存在循环引用的问题,因为EmployeeModule已经被AuthModule引用了
  • 所以我们可以把AuthModule通过装饰器@Global(),设置为全局模块
  • 更改auth.module.ts,添加@Global()
    import { Global, Module } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { EmployeeModule } from '../employee/employee.module';
    import { PassportModule } from '@nestjs/passport';
    import { JwtModule } from '@nestjs/jwt';
    import { getConfig } from 'src/common/utils/ymlConfig';
    import { LocalStrategy } from './strategy/local.strategy';
    import { JwtStrategy } from './strategy/jwt.strategy';
    
    @Global()
    @Module({
      imports: [
        EmployeeModule,
        PassportModule,
        JwtModule.register({ ...getConfig('JWT') }),
      ],
      providers: [AuthService, LocalStrategy, JwtStrategy],
      exports: [AuthService],
    })
    export class AuthModule {}
    
  • image.png
  • employee.controller.ts中直接使用即可
  • image.png
测试
  • 启动项目,打开swagger进行测试 http://localhost:3000/api
  • username输入admin,密码输入123456
  • image.png
  • 发现token被正确的返回
  • 接下来把返回的token,放到test接口的请求头中进行测试
  • image.png
  • 发现没有问题
  • 接着我们对token进行更改,再进行测试,发现已经被拦截
  • image.png
免拦截
  • 认证技术固然好使,但有些接口是不需要被认证的
  • 对于不需要认证的接口,只需要在方法上加入注解@isPublic即可
  • image.png

总结

  • 到现在nest部分的认证和登陆功能都开发完毕,对于前端来说,认证相关部分,比较难以理解,需要一定的时间去熟悉整个调用链
  • 数据库设计部分,不是一下就能设计的很棒,需要多理解业务需求,根据业务场景进行不断的调整
  • 登陆功能login方法中的代码其实比较简单,就是做各种判断,然后抛出异常,注意的点就是,部分敏感数据是不需要返回的
  • 总结下来,就是需要多理解业务需求,逻辑要求比较严谨

写在最后

  • 本章主要讲解认证登陆,如有问题欢迎在评论区留言
  • 本系列不仅仅在熟悉nest套路,还有对ts的学习,比如联合类型 交叉类型 类型合并 泛型等等的使用,希望有帮助
  • 前端仓库nest-study-bacnend
  • nest代码已经放在 gitee demo/v3分支
  • mysql不熟悉的可以看下 前端玩转mysqlNodejs连接Mysql 这俩篇文章
  • 对Nest语法不熟悉的掘友可以看下Nest文档Midway文档搭配服用效果更佳