Nestjs学习笔记

361 阅读20分钟

推荐一位大咖级博主,讲的都是干货,此次学习内容主要也来源于他,他就是后盾人大叔 后盾人 (houdunren.com)

还有CSDN的小满博主,大家也可以关注下:小满nestjs(第二章 IOC控制反转 DI依赖注入)_ioc控制反转 nestjs-CSDN博客

nestjs的生命周期

image.png

我们可以看出,nestjs的生命周期如上,首先从客户端出发,会经过中间件,守卫,拦截器,管道等,其实每个步骤都是对请求进行拦截然后进行处理,我们甚至可以在任何一个中做其他的所有事情,但是这么区分步骤可以让我们处理的更加清晰,下面我们会详细说明每个生命周期都是做什么用的。

初始化一个项目

全局安装

pnpm add -g @nestjs/cli nodemon ts-node
nest new project-name

依赖安装

pnpm add prisma-binding ts-node @prisma/client mockjs @nestjs/config class-validator class-transformer argon2 @nestjs/passport passport passport-local @nestjs/jwt passport-jwt lodash multer dayjs express redis @nestjs/throttler mockjs @nestjs/cache-manager cache-manager md5 @casl/prisma @casl/ability

pnpm add -D prisma typescript @types/node @types/mockjs @nestjs/mapped-types @types/passport-local @types/passport-jwt @types/express @types/lodash @types/multer @types/cache-manager @types/md5

简单案例

src/main.ts

main.ts是入口函数

import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'//主模块

async function bootstrap() {
  const app = await NestFactory.create(AppModule)//执行工厂函数创建app
  await app.listen(3000)//监听3000端口
}
bootstrap()

src/app.module.ts

app.module是主模块,所有模块最后都注入在这里即可

import { Module } from '@nestjs/common'

import { AuthModule } from './auth/auth.module'

@Module({
  imports: [AuthModule],//注入AuthModule模块
  controllers: [],
  providers: [],
})
export class AppModule {}

src/auth/auth.module.ts

auth.module是用户权限模块,里面来写用户登录、注册的逻辑

import { Module } from '@nestjs/common'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'

@Module({
  controllers: [AuthController],//注入控制器
  providers: [AuthService],//注入提供者
})
export class AuthModule {}

src/auth/auth.controller.ts

用户权限模块中的控制器,主要来写路由

import { Controller, Get } from '@nestjs/common'
import { AuthService } from './auth.service'

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Get()
  login() {
    return this.authService.login()
  }
}

src/auth/auth.service.ts

服务文件,主要是来处理用户访问路由后的逻辑,进行返回数据等 这里的Injectable()是依赖注入,在此处写完之后,需要在module中的provider中进行注入,这样就可以在控制器中直接使用constructor(private readonly authService: AuthService) {}

import { Injectable } from '@nestjs/common'

@Injectable() //依赖注入,在此处写用
export class AuthService {
  login() {
    return '登录了'
  }
}

下面我们使用apiFox进行测试,发现已经成功了

装饰器

装饰器是TS中的试验性内容,写法为@Fn(),装饰器的作用为给函数、类、方法、属性添加声明,这里具体可以去看TS官方文档Decorators - TypeScript 中文手册 (bootcss.com)

依赖注入

什么是依赖注入呢?依赖注入其实就是控制反转的实现方法,下面我们用一个例子来解释

例A

我们假设有三个类,A类,B类和C类

   
    class A {
 name: string
 constructor(name: string) {
  this.name = name
 }
  }


class B {
    age:number
    entity:A
    constructor (age:number) {
        this.age = age;
        this.entity = new A('小满')
    }
}
 
const c = new B(18)
————————————————
版权声明:本文为CSDN博主「小满zs」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq1195566313/article/details/126151370

我们可以看到,在B中需要实现A类的实例,而C中又需要使用B的实例,因此这里是强耦合关系,那么会存在一个问题,就是当A发生改变,B也需要发生改变,这样不利于代码维护,因此这里可以用一个容器来解耦


class A {
    name: string
    constructor(name: string) {
        this.name = name
    }
}
 
 
class C {
    name: string
    constructor(name: string) {
        this.name = name
    }
}
//中间件用于解耦
class Container {
    modeuls: any
    constructor() {
        this.modeuls = {}
    }
    provide(key: string, modeuls: any) {
        this.modeuls[key] = modeuls
    }
    get(key) {
        return this.modeuls[key]
    }
}
 
const mo = new Container()
mo.provide('a', new A('小满1'))
mo.provide('c', new C('小满2'))
 
class B {
    a: any
    c: any
    constructor(container: Container) {
        this.a = container.get('a')
        this.c = container.get('c')
    }
}
 
new B(mo)
————————————————
版权声明:本文为CSDN博主「小满zs」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq1195566313/article/details/126151370

其实这里就是把类和类之间的依赖关系进行解耦,从而减少代码的耦合量。

Providers

Providers 是 Nest 的一个基本概念。许多基本的 Nest 类都可能被视为 provider - service, repositoryfactoryhelper 等等。 他们都可以通过 constructor 注入依赖关系。 这意味着对象可以彼此创建各种关系,并且“连接”对象实例的功能在很大程度上可以委托给 Nest运行时系统。 Provider 只是一个用 @Injectable() 装饰器注释的类。

结合上一章节中的依赖注入,Providers正好是与他结合起来,正如我们在全文第一个初始化项目中那样,在服务上我们写上了injectable,这就说明此服务被依赖注入了,而如果我们想使用,就需要在providers进行写入

@Injectable()
export class AuthService {
  login() {
    return '登录了'
  }
}

@Module({
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

一旦在providers中使用,在控制器中我们就可以不new这个服务的实例,而直接这么写即可

constructor(private readonly authService: AuthService) {}

这样写之后就可以直接使用authService这个类

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Get()
  login() {
    return this.authService.login()
  }
}

管道pipe

管道的作用

  • 将前端传过来是类型转换成后端需要的[比如string转换成number]
  • 对前端传递的数据进行验证,如果不符合就抛出异常

nestjs给我们提供了许多获取前端参数的方法

image.png

案例A-内置管道

  • 控制器中
  @Get('login')
  login(@Query('id' ) id) {
    return this.authService.login(id)
  }
  • 服务中
  login(id) {
    console.log('id: ', id, typeof id)
    return '登录了'
  }

因此我们可以通过body获取到前端入参

image.png 但是后端需要的age是number类型,而前端传递过来的是字符串类型,因此可以使用管道对其类型进行转换,可以 在控制器中使用内置管道ParseIntPipe

  @Get('login')
  login(@Query('id', ParseIntPipe) id) {
    return this.authService.login(id)
  }

image.png

案例B-自定义管道

前端入参会先经过管道,管道处理之后再出去被使用

新建文件夹 src/common/validation/validation.pipe.ts

// 自定义管道,这样前端入参就会先走管道,然后再出来
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    throw new BadRequestException('参数错误')
     return value
  }
}

直接在参数后边使用,例如我们抛出错误,这时管道验证后就不会进入控制器中的函数

@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Get('login')
  // 自定义管道--ValidationPipe,自定义管道使用在参数后边,入参会先走管道,然后再出来,
  // 此时我们可以对数据进行验证
  login(@Query('id', ValidationPipe) id) {
    return this.authService.login(id)
  }

  @Post('register')
  register(@Body('age', ValidationPipe) dto) {
    return this.authService.register(dto)
  }
}

image.png

下面我们打印数据,看看

   console.log('metadata: ', metadata)
    console.log('value: ', value)

其中metadata是元数据 image.png

我们可以在自定义管道中来处理前端入参,比如把string转换成number,这样我们就相当于自己定义了内置管道ParseIntPipe

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (typeof value == 'number') {
      return value
    } else {
      return +value
    }
  }
}

注:管道不止可以放在参数前,其实也可以放在请求方法上、控制器上【将会作用控制器底下的所有路由】、模块的提供者上及全局管道

  • 用在请求函数上
@Controller()
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Get('login')
  // 自定义管道--ValidationPipe,自定义管道使用在参数后边,入参会先走管道,然后再出来,
  // 此时我们可以对数据进行验证
   @UsePipes(ValidationPipe)
  login(@Query('id') id) {
    return this.authService.login(id)
  }

  @Post('register')
  @UsePipes(ValidationPipe)
  register(@Body('age') dto) {
    return this.authService.register(dto)
  }
}
  • 用在控制器上

@Controller()
  @UsePipes(ValidationPipe)
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Get('login')
  // 自定义管道--ValidationPipe,自定义管道使用在参数后边,入参会先走管道,然后再出来,
  // 此时我们可以对数据进行验证
  login(@Query('id') id) {
    return this.authService.login(id)
  }

  @Post('register')

  register(@Body('age') dto) {
    return this.authService.register(dto)
  }
}
  • 用在全局上
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe()) //全局管道注入,main.ts没有在模块上,所以依赖注入不能用,因此需要手动new一下
  await app.listen(3000)
}
bootstrap()

管道的验证

上面是管道对数据的转换,下面来看验证 假设前端传参中,年龄不能为空,我们在管道中可以这么写

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any) {
    if (!value.age) {
      console.log('@@@@')
      throw new BadRequestException('年龄不能为空')
    }
    return value
  }
}
image.png

但是如果对于每个参数都这样手动判断,则太过麻烦且做不到复用,因此可以利用数据传输对象DTO进行验证

首先建立一个createPersonInfo.dto.ts

由于TS编译成JS之后,类型声明会丢失,因此我们使用类来进行接口撰写
export default class PersonInfoDto {
  name: string
  age: number
}

 @Post('register')
  @UsePipes(ValidationPipe)
  register(@Body() dto: PersonInfoDto) {
    return this.authService.register(dto)
  }

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    console.log('metadata: ', metadata)
    console.log('value: ', value)

    return value
  }
}

image.png

此时我们再打印元信息metaData就发现他是我们定义的那个类,那么我们要把我们的数据和这个类有关联,即按照这个类把他给实例化出对象,那如何做呢?

我们借助工具npm i --save class-validator class-transformer

    const object = plainToInstance(metadata.metatype, value)//这样就能把这个数据按照类实例化出来,也就是说我们提供的表单数据会通过这个dto给创建出来,因此我们可以在创建的过程中就进行验证

class-validator这个包里有一些验证功能,比如不能为空,长度限制等;即当我用前端传递过来的数据value通过构造函数来实例化对象的时候,这些装饰器会来判断这个属性,让他不能为空,或者长度限制,如果有错误,就会抛出异常,他的做法就等效于我们在上面自己判断

 if (!value.age) {
      console.log('@@@@')
      throw new BadRequestException('年龄不能为空')
    }
export default class PersonInfoDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  name: string
  age: number
}

然后我们可以通过包里的validate来得到验证的结果,之后再抛出异常

@Injectable()
export class ValidationPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const object = plainToInstance(metadata.metatype, value)
    const errors = await validate(object)
    console.log('errors: ', errors);
    if (errors.length) {
      throw new BadRequestException('表单验证失败')
    }
    return value
  }
}

image.png

自定义管道的验证

image.png

现在我们已经比之前高效了很多,但是我们打印这个error会发现是这样的,这个错误信息是个数组,但是结构比较复杂,我们可以对其进行自定义 ,取出错误消息然后组装的规范一点


@Injectable()
export class ValidationPipe implements PipeTransform {
  async transform(value: any, metadata: ArgumentMetadata) {
    const object = plainToInstance(metadata.metatype, value)
    const errors = await validate(object)
    const message = errors.map((error) => {
      return {
        name: error.property,
        message: Object.values(error.constraints),
      }
    })
    console.log(message)
    if (errors.length) {
      throw new BadRequestException('表单验证失败')
    }
    return value
  }
}

image.png

此时我们的错误消息已经比较规范,我们还可以把状态码等封装进去,但此刻我们先停一下,讲一下过滤器

过滤器

虽然基本(内置)异常过滤器可以为您自动处理许多情况,但有时您可能希望对异常层拥有完全控制权,例如,您可能希望基于某些动态因素添加日志记录或使用不同的 JSON 模式。 异常过滤器正是为此目的而设计的。 它们使您可以控制精确的控制流以及将响应的内容发送回客户端。

让我们创建一个异常过滤器,它负责捕获作为HttpException类实例的异常,并为它们设置自定义响应逻辑。为此,我们需要访问底层平台 Request和 Response。我们将访问Request对象,以便提取原始 url并将其包含在日志信息中。我们将使用 Response.json()方法,使用 Response对象直接控制发送的响应。

综上,使用过滤器的目的就是把错误变成有格式的,方便阅读。我们可以通过过滤器来捕获异常,然后来处理异常,包括表单验证的时候,当抛出一个错误,比如抛出状态码400,此时就需要一个过滤器来捕获抛出来的信息,然后返回给客户端

我们创建一个过滤器,src/common/http-exception.filter.ts,然后在main.ts中全局应用

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'
import { Request, Response } from 'express'

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    const status = exception.getStatus()

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    })
  }
}
async function bootstrap() { 
const app = await NestFactory.create(AppModule); 
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000); 
} 
bootstrap();

此时我们如果再次发送请求,发现请求是有格式的,如下:

image.png 在上面的示例中,过滤器将捕获抛出的每个异常,而不管其类型(类)如何。

基于过滤器,我们就可以把表单验证的错误变成有格式的内容

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, BadRequestException, HttpStatus } from '@nestjs/common'
import { Request, Response } from 'express'

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const responseObj = exception.getResponse() as any

    response.status(HttpStatus.UNPROCESSABLE_ENTITY).json({
      code: HttpStatus.UNPROCESSABLE_ENTITY,//状态码用422
      message: responseObj,
    })
  }
}

image.png

到这里,过滤器就完成啦!

自定义验证类实现密码比对

类验证器的文档class-validator - NestJs 文档 (wdk-docs.github.io) 在我们注册时,我们要敲密码和确认密码,此时两个密码后端需要进行比对 首先我们新建一个rules文件:src/rules/is-consifrm.rule.ts

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator'

@ValidatorConstraint({ name: 'customText', async: false })
export class CustomTextLength implements ValidatorConstraintInterface {
  validate(text: string, args: ValidationArguments) {
    return text.length > 1 && text.length < 10 // for async validations you must return a Promise<boolean> here
  }

  defaultMessage(args: ValidationArguments) {
    // here you can provide default error message if validation failed
    return 'Text ($value) is too short or too long!'
  }
}

我们在dto中进行使用即可

import { IsNotEmpty, Validate } from 'class-validator'
import { CustomTextLength } from 'src/rules/is-consifrm.rule'
export default class PersonInfoDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  @Validate(CustomTextLength)
  password: string
}

此时我们再次发送请求,密码设置为大于10位,这时我们发现自定义的类验证器就实现效果了!

image.png

下面我们来实现注册时密码是否一致性的校验

首先在dto中设置密码和确认密码

import { IsNotEmpty, Validate } from 'class-validator'
import { CustomTextLength } from 'src/rules/is-consifrm.rule'
export default class PersonInfoDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  @Validate(CustomTextLength)
  password: string
  @Validate(CustomTextLength)
  password_confirm: string
}
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator'

@ValidatorConstraint({ name: 'customText', async: false })
export class CustomTextLength implements ValidatorConstraintInterface {
  validate(text: string, args: ValidationArguments) {
    console.log('====================================')
    console.log(text, args)
    console.log('====================================')
    return false
  }

  defaultMessage(args: ValidationArguments) {
    // here you can provide default error message if validation failed
    return 'Text ($value) is too short or too long!'
  }
}

我们打印一下text和args看一下,发现第一个形参是当前的值,第二个参数中是PersonInfoDto中所有的值,此时我们如果想做密码比对验证,那么可以这么操作 image.png

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator'

@ValidatorConstraint({ name: 'customText', async: false })
export class CustomTextLength implements ValidatorConstraintInterface {
  validate(text: string, args: ValidationArguments) {
    console.log(text, args)
    return text === args['object'][args.property + '_confirm']
  }

  defaultMessage(args: ValidationArguments) {
    // here you can provide default error message if validation failed
    return '两次密码输入不一致'
  }
}

image.png 至此,密码唯一性验证就完成啦!

使用装饰器完成用户唯一验证

image.png > 比如我们注册了A用户,此时该用户就入库了,如果我们此时又注册一个A用户,那么就会重复,因此需要完成唯一验证

我们创建一个src/rules/is-not-exist.rule.ts

import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'

export function isNotExistRule(property: string, validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: 'isNotExistRule',
      target: object.constructor,
      propertyName: propertyName,
      constraints: [property],
      options: validationOptions,
      validator: {
        validate(value: any, args: ValidationArguments) {
          return false  //在这里retrun boolean值
        },
      },
    })
  }
}

import { IsNotEmpty, Validate } from 'class-validator'
import { CustomTextLength } from 'src/rules/is-consifrm.rule'
import { isNotExistRule } from 'src/rules/is-not-exist.rule'
export default class PersonInfoDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @isNotExistRule('user', { message: '用户名已存在' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  @Validate(CustomTextLength)
  password: string

  password_confirm: string
}

此时我们发送请求可以得到:

image.png

那么此时我们可以利用prismaClient来在表中进行查找,如果查找到,说明用户已经注册,则返回false

import { PrismaClient } from '@prisma/client'
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'

export function isNotExistRule(table: string, validationOptions?: ValidationOptions) {
  return function (object: any, propertyName: string) {
    registerDecorator({
      name: 'isNotExistRule',
      target: object.constructor,
      propertyName: propertyName,
      constraints: [table],
      options: validationOptions,
      validator: {
        async validate(value: string, args: ValidationArguments) {
          const prisma = new PrismaClient()
          const user = await prisma[table].findFirst({
            where: {
              [propertyName]: args.value,
            },
          })

          return !Boolean(user) //在这里retrun boolean值
        },
      },
    })
  }
}

image.png 此时,使用装饰器验证完成唯一用户注册就完成啦!

实现简单注册登录案例

接下来我们用上面学习的内容来实现一个简单的登录注册

初始化模块

  1. 首先我们创建一个新的模块,在终端输入nest -h可得到

image.png

创建新的模块 nest g mo auth;创建控制器和服务,nest g co authnest g s auth,这里的auth指的是模块的名称

此时我们在控制器写一个测试案例

import { Controller, Post } from '@nestjs/common'

@Controller('authLogin')
export class LoginController {
  @Post('login')
  login() {
    return '登录了'
  }
}

image.png 我们发现该模块就增加完成了,nestjs的魅力就在于如此之快!

  1. 定义prisma文件内容
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model login {
  id       Int     @id @default(autoincrement())
  name     String? @unique
  password String
}

model register {
  id       Int     @id @default(autoincrement())
  name     String? @unique
  password String
  password_confirmed String
}

创建seed.ts文件,seed.ts是数据填充文件,首先在package.json中定义命令,后面可以调用 npx prisma db seed 命令实现填充

{
  "name": "nest",
  ...
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  },
  "scripts": {
  ...
  }
}

然后你需要创建prisma/seeds目录 ,该目录用于定义数据填充文件。

import { PrismaClient } from '@prisma/client'
import * as bcrypt from 'bcryptjs'
const prisma = new PrismaClient()
const run = async () => {
  await prisma.user.create({
    data: {
      name: 'admin',
      password: await bcrypt.hashSync('123456'),
    },
  })
}
run()

这里我们使用了bcryptjs来进行密码加密,seed.ts的作用就是初始化数据,比如初始化一个管理员,这个管理员有权限进行增删改查等

现在执行命令就可以有填充数据了

npx prisma db seed

执行以下命令会自动执行数据填充

npx prisma migrate reset

添加prisma模块

从上面学习得知,当我们想操作mysql库时,通常都需要利用PrismaClient客户端来进行增删改查,为了方便各个服务都能使用Prisma服务,我们可以新建一个Prisma模块,然后把PrismaClient全局导出

创建新的模块 nest g mo prisma;创建控制器和服务,nest g s prisma,这里我们不需要控制器,因为我们只是暴露一个服务出去

我们可以使用  @Global 装饰器来将模块声明为全局模块,然后设置模块 prisma/prisma.module.ts 注册提供者,并使用exports选项向外部提供 PrismaService 服务

现在其他模块也可以使用 PrismaService 服务了

image.png

注册

  • 首先创建数据传输对象DTO文件进行表单验证
import { IsNotEmpty } from 'class-validator'
export default class AuthRegisterDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  password: string
  @IsNotEmpty({ message: '确认密码不能为空' })
  password_confirmed: string
}
  • 此时我们完成了简单的注册
import { Body, Controller, Post } from '@nestjs/common'
import { AuthService } from './auth.service'
import AuthRegisterDto from './auth.dto'

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('register')
  register(@Body() dto: AuthRegisterDto) {
    return this.authService.register(dto)
  }
}
import { Injectable } from '@nestjs/common'
import { PrismaService } from 'src/prisma/prisma.service'

@Injectable()
export class AuthService {
  constructor(private readonly prisma: PrismaService) {}
  async register(dto) {
    console.log(dto)
    const isUser = await this.prisma.user.findFirst({
      where: {
        name: dto.name,
      },
    })
    if (isUser) {
      return '该用户已经注册'
    }
    console.log(isUser)

    return '这里是注册'
  }
}

结合上面学习内容【使用装饰器完成用户唯一验证】及【自定义类实现密码对比】,我们可以把用户唯一性验证封装起来

import { IsNotEmpty, Validate } from 'class-validator'
import { CustomPasswordVerify } from 'src/rules/is-consifrm.rule'
import { isNotExistRule } from 'src/rules/is-not-exist.rule'

export default class AuthRegisterDto {
  @IsNotEmpty({ message: '用户名不能为空' })
  @isNotExistRule('user', { message: '用户名已存在' })
  name: string
  @IsNotEmpty({ message: '密码不能为空' })
  @Validate(CustomPasswordVerify)
  password: string
  @IsNotEmpty({ message: '确认密码不能为空' })
  password_confirmed: string
}

由于我的mysql库里已经有了admin这个用户,此时我注册一个新的admin,就会得到

image.png
  • 完善注册的密码加密

由于注册登录时要对用户的密码进行保密,因此我们要对其进行加密,我们可以引入bcryptjs包来实现 bcryptjs - npm (npmjs.com)

import { Injectable } from '@nestjs/common'
import { PrismaService } from 'src/prisma/prisma.service'
import * as bcrypt from 'bcryptjs'
@Injectable()
export class AuthService {
  constructor(private readonly prisma: PrismaService) {}
  async register(dto) {
    //当进入到这里时,说明已经通过了管道里的验证
    //我们首先对密码进行加密
    const password = await bcrypt.hashSync('123456')
    await this.prisma.user.create({
      data: {
        name: dto.name,
        password,
      },
    })
    delete dto.password
    delete dto.password_confirm
    return dto
  }
}
image.png 到这里,注册就基本完成了,下面我们来完成登录

登录

登录比注册要简单很多,其实就是拿到用户的账号密码去数据库里比对,如果成功,则进行登录

实现JWT认证

身份验证过程是,客户端将首先使用用户名和密码进行身份验证。经过身份验证后,服务器将发出一个 JWT。然后在请求的头信息中携带 JWT 来标识身份。

我们需要完成以下几步

  • 对用户进行身份验证
  • 然后,我们将发给用户token
  • 最后,我们将创建一个受保护的路由,用于检查请求上的有效token

安装JWT依赖

使用JWT需要安装 @nest/jwt (opens new window)等依赖包


pnpm add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
pnpm add -D @types/passport-local @types/passport-jwt

基于我们上面的注册登录项目,我们在auth.module.ts模块中定义Jwt模块


import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';

@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        return {
          //设置加密使用的 secret
          secret: config.get('app.token_secret'),
          //过期时间
          signOptions: { expiresIn: '300d' },
        };
      },
    }),
  ],
  providers: [AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

获取令牌

在用户登录成功后,返回token

  • .env文件
# 数据库链接
DATABASE_URL="mysql://root:123456@127.0.0.1:3306/nest-blog"

# token秘钥
TOKEN_SECRET="xuhuaiyu"
  • auth.module.ts
import { Module } from '@nestjs/common'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { JwtModule } from '@nestjs/jwt'
@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        return {
          //设置加密使用的 secret
          secret: config.get('TOKEN_SECRET'),
          //过期时间
          signOptions: { expiresIn: '300d' },
        }
      },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

  • auth.service.ts
import { BadRequestException, Injectable } from '@nestjs/common'
import { PrismaService } from 'src/prisma/prisma.service'
import * as bcrypt from 'bcryptjs'
import { LoginDto } from './auth.dto'
import { JwtService } from '@nestjs/jwt'
import { user } from '@prisma/client'
@Injectable()
export class AuthService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly jwt: JwtService,
  ) {}
  async register(dto) {
    //当进入到这里时,说明已经通过了管道里的验证
    //我们首先对密码进行加密
    const password = await bcrypt.hashSync('123456')
    await this.prisma.user.create({
      data: {
        name: dto.name,
        password,
      },
    })
    delete dto.password
    delete dto.password_confirm
    return dto
  }

  async login(dto: LoginDto) {
    const user = await this.prisma.user.findUnique({
      where: { name: dto.name },
    })
    if (!user) throw new BadRequestException('用户不存在')
    const psMatch = await bcrypt.compare(dto.password, user.password)
    if (!psMatch) throw new BadRequestException('密码输入错误')
    return this.token(user)
  }
  async token(user: user) {
    return {
      token: await this.jwt.signAsync({
        username: user.name,
        sub: user.id,
      }),
    }
  }
}

此时当我们进行登录时,会得到

image.png

身份校验

这里我们先写一个获取所有用户的方法

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @Get('all')
  all() {
    return this.authService.getAllUser()
  }
}
@Injectable()
export class AuthService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly jwt: JwtService,
  ) {}
  async getAllUser() {
    const users = await this.prisma.user.findMany()
    console.log('users: ', users)
  }
}

此时我们发送请求就会获得数据库里的所有用户,但是这没加token,按理说不应该返回内容,所以我们要让只有登录用户才能获得内容

下面来使用token进行身份验证,首先定义 jwt.strategy.ts 文件,定义使用 token 进行身份验证的JWT策略。


import { PrismaService } from './../prisma/prisma.service';
import { ConfigService } from '@nestjs/config';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(configService: ConfigService, private prisma: PrismaService) {
    super({
      //解析用户提交的header中的Bearer Token数据
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      //加密码的 secret
      secretOrKey: configService.get('TOKEN_SECRET'),
    });
  }

  //验证通过后获取用户资料
  async validate({ sub: id }) {
    return this.prisma.users.findUnique({
      where: { id }, 
    });
  }
}

然后在 auth.module.ts 中注册为提供者


import { JwtStrategy } from './strategy';
...
@Module({
  ...
  controllers: [AuthController],
  //注入容器
  providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

然后使用即可

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}
  @UseGuards(AuthGuard('jwt'))
  @Get('all')
  all() {
    return this.authService.getAllUser()
  }
}

Prisma的使用技巧

修改已有表字段

在prisma中,如果mysql表中已经有字段,比如叫configName,我们想给他改成name,应该如何做呢?

image.png 由于表中已经有数据存在,如果我们直接在schema中修改,然后migrate dev,那么一定会报错,因此我们可以先migrate dev --create-only,此时会创建一条迁移文件,但是并没有应用

image.png

此时迁移文件中会这样


ALTER TABLE `app_config` DROP COLUMN `configName`,
    ADD COLUMN `name` VARCHAR(191) NOT NULL;

上面代码是先删除configName,再新增name,但是由于表中有数据,此时migrate dev会报错,因为我们可以这样操作,在迁移文件中改SQL语句


ALTER TABLE `app_config` CHANGE COLUMN `configName` `name` VARCHAR(191) NOT NULL;

此时再 migrate dev即可

影子数据库

如果使用prisma,并且数据库在远程服务器,一定要根据文档创建一个影子数据库

参考文献:

秘籍 (nestjs.cn)

Prisma 中文网 (nodejs.cn)