前言
本系列主要通过实现一个后台管理系统作为切入点,帮助掘友熟悉nest
的开发套路
本系列作为前后端分离项目,主要讲解nest
相关知识,前端项目知识一笔带过
由于上一章的通用模板中有测试代码,我重新上传了一份干净的代码demo/v2,如下
完整的示例代码在最后
本章属于实战系列第一章,主要讲解登陆模块相关内容,包括
- MD5加密
- 数据库设计
- JWT认证
- Nest知识点(模块循环引用、自定义装饰器)
- ts 赋值合并、交叉类型 联合类型
- 等等
技术栈
- node:19.x
- 后端:nest + mysql
- 前端:react + redux + antd
数据库设计
employee
表设计如下
表介绍
- id 为主键自增
- 用户名和身份证号做了唯一约束,防止重名
- 本系列每张表都会有创建时间、更新时间、创建人和更新人,主要为了留痕
- 其他的字段都比较中规中矩
导入数据
- 打开
Navicat
- 选择
sql
文件,在项目根目录/doc/nest-study.sql
,点击开始 - 出现这样的界面就可以了
- 然后刷新表就可以看到数据了
- 这样数据库就设计好了,接下来就可以开始开发了
登陆模块开发
新建模块
- 命令行执行
nest g res employee
- 第一步选择
REST API
- 第二步选择
y
- 第一次创建模块有点慢,耐心等待即可
- 出现下面的内容,模块就创建成功了
优化模块内容
- 删除
dto
下的文件夹,如无特殊情况直接使用实体类即可 - 删除
service
和controller
中生成的方法,保留干爽内容
更新实体类
- 打开
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
即可
登陆开发
集成数据库到employee
模块中
- 修改
employee.module.ts
, 注入实体Employee
到employee
模块中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, },
- 如下所示
接口开发
代码开发
- 安装
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
测试结果
- 我们通过输入错误的账户名、错误的密码以及正确的账户名和密码,发现和我们代码中的逻辑是一样的
认证(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
,导出EmployeeService
供Auth
模块使用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
加到nest
的IOC
中 - 修改
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认证
,最后做