Nest实战 - 认证登陆

lxf2023-03-12 10:23:01

前言

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

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

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

Nest实战 - 认证登陆

完整的示例代码在最后

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

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

技术栈

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

数据库设计

employee表设计如下

  • Nest实战 - 认证登陆

介绍

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

导入数据

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

登陆模块开发

新建模块

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

优化模块内容

  • 删除dto下的文件夹,如无特殊情况直接使用实体类即可
  • 删除servicecontroller 中生成的方法,保留干爽内容
  • Nest实战 - 认证登陆

更新实体类

  • 打开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即可
  • Nest实战 - 认证登陆

登陆开发

集成数据库到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,
        },
    
  • 如下所示
  • Nest实战 - 认证登陆

接口开发

代码开发
  • 安装 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

  • Nest实战 - 认证登陆

  • Nest实战 - 认证登陆

  • Nest实战 - 认证登陆

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

认证(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认证,最后做