推荐一位大咖级博主,讲的都是干货,此次学习内容主要也来源于他,他就是后盾人大叔 后盾人 (houdunren.com)
还有CSDN的小满博主,大家也可以关注下:小满nestjs(第二章 IOC控制反转 DI依赖注入)_ioc控制反转 nestjs-CSDN博客
nestjs的生命周期
我们可以看出,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,repository,factory,helper等等。 他们都可以通过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给我们提供了许多获取前端参数的方法
案例A-内置管道
- 控制器中
@Get('login')
login(@Query('id' ) id) {
return this.authService.login(id)
}
- 服务中
login(id) {
console.log('id: ', id, typeof id)
return '登录了'
}
因此我们可以通过body获取到前端入参
但是后端需要的age是number类型,而前端传递过来的是字符串类型,因此可以使用管道对其类型进行转换,可以 在控制器中使用内置管道ParseIntPipe
@Get('login')
login(@Query('id', ParseIntPipe) id) {
return this.authService.login(id)
}
案例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)
}
}
下面我们打印数据,看看
console.log('metadata: ', metadata)
console.log('value: ', value)
其中metadata是元数据
我们可以在自定义管道中来处理前端入参,比如把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
}
}
但是如果对于每个参数都这样手动判断,则太过麻烦且做不到复用,因此可以利用数据传输对象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
}
}
此时我们再打印元信息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
}
}
自定义管道的验证
现在我们已经比之前高效了很多,但是我们打印这个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
}
}
此时我们的错误消息已经比较规范,我们还可以把状态码等封装进去,但此刻我们先停一下,讲一下过滤器
过滤器
虽然基本(内置)异常过滤器可以为您自动处理许多情况,但有时您可能希望对异常层拥有完全控制权,例如,您可能希望基于某些动态因素添加日志记录或使用不同的
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();
此时我们如果再次发送请求,发现请求是有格式的,如下:
在上面的示例中,过滤器将捕获抛出的每个异常,而不管其类型(类)如何。
基于过滤器,我们就可以把表单验证的错误变成有格式的内容
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,
})
}
}
到这里,过滤器就完成啦!
自定义验证类实现密码比对
类验证器的文档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位,这时我们发现自定义的类验证器就实现效果了!
下面我们来实现注册时密码是否一致性的校验
首先在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中所有的值,此时我们如果想做密码比对验证,那么可以这么操作
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 '两次密码输入不一致'
}
}
至此,密码唯一性验证就完成啦!
使用装饰器完成用户唯一验证
我们创建一个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
}
此时我们发送请求可以得到:
那么此时我们可以利用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值
},
},
})
}
}
此时,使用装饰器验证完成唯一用户注册就完成啦!
实现简单注册登录案例
接下来我们用上面学习的内容来实现一个简单的登录注册
初始化模块
- 首先我们创建一个新的模块,在终端输入nest -h可得到
创建新的模块 nest g mo auth;创建控制器和服务,nest g co auth、nest g s auth,这里的auth指的是模块的名称
此时我们在控制器写一个测试案例
import { Controller, Post } from '@nestjs/common'
@Controller('authLogin')
export class LoginController {
@Post('login')
login() {
return '登录了'
}
}
我们发现该模块就增加完成了,nestjs的魅力就在于如此之快!
- 定义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 服务了
注册
- 首先创建数据传输对象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,就会得到
- 完善注册的密码加密
由于注册登录时要对用户的密码进行保密,因此我们要对其进行加密,我们可以引入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
}
}
登录
登录比注册要简单很多,其实就是拿到用户的账号密码去数据库里比对,如果成功,则进行登录
实现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模块
- 过期时间设置请参考 vercel/ms (opens new window)扩展包
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,
}),
}
}
}
此时当我们进行登录时,会得到
身份校验
这里我们先写一个获取所有用户的方法
@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,应该如何做呢?
由于表中已经有数据存在,如果我们直接在schema中修改,然后migrate dev,那么一定会报错,因此我们可以先migrate dev --create-only,此时会创建一条迁移文件,但是并没有应用
此时迁移文件中会这样
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,并且数据库在远程服务器,一定要根据文档创建一个影子数据库