nestjs之个人博客项目

360 阅读7分钟

初始化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()

image.png

这样就代表着我们的文章和栏目有了关联关系了

创建栏目模块

首先创建一个模块 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()
  }
}

这样我们就可以完成角色守卫的定义,此处建议多看看官方文档