初始化nestjs项目
使用pnpm初始化nstjs项目,并生成auth[登录、注册]模块,具体步骤见Nestjs学习笔记 - 掘金 (juejin.cn)
自定义表单验证错误类
创建完dto文件之后,我们需要自定义表单验证错误类
- src/common/validation/validation.pipe.ts
// 自定义管道,这样前端入参就会先走管道,然后再出来
import { PipeTransform, Injectable, ArgumentMetadata, HttpStatus, HttpException } from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToInstance } from 'class-transformer'
@Injectable()
export class ValidationPipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
//前台提交的表单数据没有类型,使用 plainToClass 转为有类型的对象用于验证
const object = plainToInstance(metadata.metatype, value)
//根据 DTO 中的装饰器进行验证
const errors = await validate(object)
if (errors.length) {
const messages = errors.map((error) => {
return {
name: error.property,
message: Object.values(error.constraints),
}
})
console.log(messages)
throw new HttpException(messages, HttpStatus.BAD_REQUEST) //nestjs中默认错误状态码是400
}
return value
}
}
然后我们在main.ts中使用即可
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ValidationPipe } from './common/validation/validation.pipe'
import { HttpExceptionFilter } from './common/validation/http-exception.filter'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe()) //全局管道注入,main.ts没有在模块上,所以依赖注入不能用,因此需要手动new一下
app.useGlobalFilters(new HttpExceptionFilter()) //全局注册过滤器
await app.listen(3000)
}
bootstrap()
自定义拦截器
拦截器是在请求前后对数据进行拦截处理。一般我们与后端会有一个约定好的数据格式,例如:
{
data:{
},
code:xxx,
total:xxx,
}
此时我们可以使用拦截器来实现,下面是使用拦截器对所有响应数据以data属性进行包裹。
src/common/transformInterceptor.ts
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common'
import { Request } from 'express'
import { map } from 'rxjs/operators'
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
console.log('拦截器前')
const request = context.switchToHttp().getRequest() as Request
const startTime = Date.now()
return next.handle().pipe(
map((data) => {
const endTime = Date.now()
new Logger().log(`TIME:${endTime - startTime}\tURL:${request.path}\tMETHOD:${request.method}`)
return {
data,
}
}),
)
}
}
然后在main.ts中进行全局使用,此时我们发送请求,得到的数据格式如下:
初始化prisma
这里具体内容看我上篇文章Nestjs学习笔记 - 掘金 (juejin.cn)
prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model user {
id Int @id @default(autoincrement())
name String? @unique
password String
}
model article {
id Int @id @default(autoincrement())
title String @unique
content String @db.Text
}
prisma/seed.ts
import { PrismaClient } from '@prisma/client'
import * as bcrypt from 'bcryptjs'
import { Random } from 'mockjs'
const prisma = new PrismaClient()
const run = async () => {
await prisma.user.create({
data: {
name: 'admin',
password: await bcrypt.hashSync('123456'),
},
})
for (let i = 0; i < 50; i++) {
await prisma.atticle.create({
data: {
title: Random.ctitle(1, 30),
content: Random.cparagraph(30, 50),
},
})
}
}
run()
我们利用 npx prisma migrate dev 来生产迁移文件,会自动执行数据填充;我们也可以执行以下命令重置数据库 npx prisma migrate reset
完成注册登录及jwt鉴权
这里具体内容看我上篇文章Nestjs学习笔记 - 掘金 (juejin.cn)
获取文章列表和分页数据
首先创建一个模块 nest g res article --no-spec,选择REST API方案,之后控制器、服务、dto都会被创建出来,我们在该模块中完成文章数据的增删改查,由于prisma的增删改查非常简单,这里我们直接上代码
article.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'
import { ArticleService } from './article.service'
import { CreateArticleDto } from './dto/create-article.dto'
import { UpdateArticleDto } from './dto/update-article.dto'
@Controller('article')
export class ArticleController {
constructor(private readonly articleService: ArticleService) {}
// 增
@Post('create')
create(@Body() createArticleDto: CreateArticleDto) {
return this.articleService.create(createArticleDto)
}
// 查所有
@Get('findAll')
findAll(@Query() query: { page: number; row: number }) {
return this.articleService.findAll(query.page, query.row)
}
// 查某一个
@Get(':id')
findOne(@Param('id') id: number) {
return this.articleService.findOne(+id)
}
// 改
@Patch(':id')
update(@Param('id') id: number, @Body() updateArticleDto: UpdateArticleDto) {
return this.articleService.update(+id, updateArticleDto)
}
// 删
@Delete(':id')
remove(@Param('id') id: number) {
return this.articleService.remove(+id)
}
}
article.service.ts
import { Injectable } from '@nestjs/common'
import { CreateArticleDto } from './dto/create-article.dto'
import { UpdateArticleDto } from './dto/update-article.dto'
import { PrismaService } from '@/prisma/prisma.service'
@Injectable()
export class ArticleService {
constructor(private readonly prisma: PrismaService) {}
create(createArticleDto: CreateArticleDto) {
return this.prisma.article.create({
data: {
title: createArticleDto.title,
content: createArticleDto.content,
},
})
}
async findAll(page = 1, row = 10) {
// 分页处理
const articles = await this.prisma.article.findMany({
skip: (page - 1) * row,
take: +row,
})
return {
meta: { current_page: page, page_row: row, total: await this.prisma.article.count() },
data: articles,
}
}
async findOne(id: number) {
const data = await this.prisma.article.findFirst({
where: {
id,
},
})
return data
}
update(id: number, updateArticleDto: UpdateArticleDto) {
return this.prisma.article.update({
where: { id },
data: updateArticleDto,
})
}
remove(id: number) {
return this.prisma.article.delete({
where: {
id,
},
})
}
}
至此,文章的增删改查我们就完成了,由于我们采用的是关系型数据库,下面我们要完成栏目和文章的一对多开发及角色验证等
栏目开发
此时我们要修改prisma模型,增加一个栏目模型
//文章
model article {
id Int @id @default(autoincrement())
title String @unique
content String @db.Text
category category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId Int
}
//栏目,和文章是一对多的关联关系
model category {
id Int @id @default(autoincrement())
title String
articles article[]
}
由于此时栏目和文章的模型改变了,因此我们要把seed种子文件修改一下,并删库重新跑一遍prisma
import { PrismaClient } from '@prisma/client'
import * as bcrypt from 'bcryptjs'
import { Random } from 'mockjs'
const prisma = new PrismaClient()
const run = async () => {
await prisma.user.create({
data: {
name: 'admin',
password: await bcrypt.hashSync('123456'),
},
})
for (let i = 0; i < 50; i++) {
await prisma.article.create({
data: {
title: Random.ctitle(1, 10),
content: Random.cparagraph(30, 50),
categoryId: Math.floor(Math.random() * 5) + 1,
},
})
}
for (let i = 1; i <= 5; i++) {
await prisma.category.create({
data: {
title: Random.ctitle(1, 6),
},
})
}
}
run()
这样就代表着我们的文章和栏目有了关联关系了
创建栏目模块
首先创建一个模块 nest g res category --no-spec,与文章类似,完成栏目的增删改查接口
token身份验证
token身份验证可以采用jwt策略,基本流程如下:
graph TD
用户登录 --> 判断账户密码是否正确 --> 登录成功发放token --> 携带token调用获取用户信息接口 --> 通过jwt策略校验token是否有效 --> 有效则放行否则401
策略机制设置
首先设置jwt策略,设置jwt策略的秘钥和过期时间 src/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'
import { JwtStrategy } from './jwt.strategy'
@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, JwtStrategy],
})
export class AuthModule {}
token的发放
在用户登录接口,调用jwt.signAsync来生成token
/src/auth/auth/service.ts
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,
}),
}
}
token的验证
首先进行jwt策略对token验证的撰写,该代码的执行逻辑为:
graph TD
super中是解析用户提交的token --> 如果解析成功则执行validate函数 --> 之后去数据库拿到用户放在request.user中 --> 在其他的守卫中可以在路由原信息中拿到user进行角色验证
src/auth/jwt.strategy.ts
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.user.findUnique({
where: { id },
})
}
}
使用token验证
src/auth/auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@UseGuards(AuthGuard('jwt'))//使用守卫来进行token验证
@Get('all')
all(@Req() req) {
console.log('req: ', req)
return this.authService.getAllUser()
}
}
角色验证
上面我们完成了token验证,即只有登录的用户才能调用获取所有用户的接口,下面我们来做角色验证,即可以控制不同角色是否可以调用接口
用户模型修改
首先在prisma中的表模型里添加角色
model user {
id Int @id @default(autoincrement())
name String? @unique
password String
role String?
}
此时要重新设置一下迁移文件 npx prisma migrate reset
聚合装饰器
聚合装饰器的官方文档:自定义装饰器 (nestjs.cn)
首先我们建立守卫
src/auth/guards/role.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Observable } from 'rxjs'
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
// 因为之前在策略中把用户信息存储在request.user中,
// 所以这里可以通过context.switchToHttp().getRequest().user获取到用户信息
const user = context.switchToHttp().getRequest().user
// 获取当前路由的roles元数据
const roles = this.reflector.get<string[]>('roles', context.getHandler())
if (roles) {
// 如果有roles元数据,则代表当前路由需要验证角色
return roles.includes(user.role)
}
return true
}
}
然后我们建立一个聚合装饰器
src/auth/decorators/auth.decorators.ts
// 聚合装饰器
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { Role } from '@/modules/auth/enum'
import { RoleGuard } from '../guards/role.guard'
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),//存储元信息
UseGuards(AuthGuard('jwt'), RoleGuard)//守卫
)
}
此时我们就可以使用角色守卫了
src/auth/auth.controller.ts
import { Body, Controller, Post, Get, UseGuards, Req } from '@nestjs/common'
import { AuthService } from './auth.service'
import { AuthRegisterDto, LoginDto } from './auth.dto'
import { AuthGuard } from '@nestjs/passport'
import { Auth } from './decorators/auth.decorator'
import { Role } from '@/modules/auth/enum'
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Auth(Role.EDITOR)
@Get('all')
all(@Req() req) {
console.log('req: ', req)
return this.authService.getAllUser()
}
}
这样我们就可以完成角色守卫的定义,此处建议多看看官方文档