07-nestjs基础实践,参数校验,管道,JWT登录鉴权,权限守卫

登录鉴权方案对比

JSON Web Tokens - jwt.io

JWT 基本概念 nest文档

JWT全称JSON Web Token,由Header,Payload,Signature组成

JWT验证流程

JWT校验原理

JWT特点

  • 防CSRF(CSRF攻击会子啊发出请求时带上Cookie,伪造请求)
  • 适合移动应用
  • 无状态,编码数据

auth模块实践

nest命令行创建auth模块

sh 复制代码
$ nest g mo auth -d # 查看将生成哪些文件
$ nest g mo auth # 生成auth模块
$ nest g s auth -d # 查看将生成哪些文件
$ nest g s auth # 生成auth服务
$ nest g co auth -d # 查看将生成哪些文件
$ nest g co auth # 生成auth控制器

实现基本的功能

修改user模块src/user/user.module.ts

ts 复制代码
// 导出UserService, 这样在AuthService中才能使用
@Module({
  + exports: [UserService],
})

修改user服务src/user/user.service.ts

ts 复制代码
// 修改dto类型为:Partial<User>,允许接收User的部分属性
- async create(dto: User) {
+ async create(dto: Partial<User>) {

编写src/auth/auth.module.ts

ts 复制代码
import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'
import { UserModule } from '../user/user.module'
@Module({
  imports: [UserModule],
  providers: [AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

编写src/auth/auth.controller.ts

ts 复制代码
import { Body, Controller, HttpException, Post, UseFilters } from '@nestjs/common'
import { AuthService } from './auth.service'
import { TypeormFilter } from '../filters/typeorm.filter'
@Controller('auth')
@UseFilters(new TypeormFilter())
export class AuthController {
  constructor(private authService: AuthService) {}
  // 用户登录
  @Post('/signin')
  singin(@Body() body: any) {
    // 此处还缺少数据校验
    const { username = '', password = '' } = body
    return this.authService.singin(username, password)
  }
  // 用户注册
  @Post('/signup')
  signup(@Body() body: any) {
    // 此处还缺少数据校验
    const { username = '', password = '' } = body
    return this.authService.signup(username, password)
  }
}

编写src/auth/auth.service.ts

ts 复制代码
import { Injectable } from '@nestjs/common'
import { UserService } from '../user/user.service'
@Injectable()
export class AuthService {
  constructor(private userService: UserService) {}
  // 用户登录
  async singin(username: string, password: string) {
    const res = await this.userService.findAll({ username } as getUserDto)
    return res
  }
  // 用户注册
  async signup(username: string, password: string) {
    const res = await this.userService.create({ username, password })
    return res
  }
}

接口参数校验和转化

利用管道校验和转化参数

安装插件:

sh 复制代码
# class-validator用于校验,class-transformer用于转化
$ npm i class-validator class-transformer

全局配置管道ValidationPipe,开启Validate管道验证接口入参, 编辑main.ts

ts 复制代码
// 在bootstrap方法中设置
+ app.useGlobalPipes(new ValidationPipe({ 
    whitelist: true // 设为true, 会删除前端传入的不在验证规则中存在的属性,提高前端安全性
})) 

创建dto参数校验对象src/auth/dto/singin-user.dto.ts

ts 复制代码
import { IsNotEmpty, IsString, Length } from 'class-validator'

export class SignginUserDto {
  @IsString()
  @IsNotEmpty()
  @Length(6, 20)
  username: string

  @IsString({ message: `密码必须是一个字符串` })
  @IsNotEmpty({ message: `密码不能为空字符串` })
  @Length(6, 20, {
    // $property: 属性名;$value: 用户传入的值;$constraint1: 参数1;$constraint2: 参数2
    message: `$property 长度要大于等于 $constraint1 且小于等于 $constraint2, 当前值:$value`
  })
  // 以下是正则验证
  @Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*?])\S*$/, {
    message: `密码要求最少6位,包含大写字母,小写字母,数字和"!@#$%^&*?"中的特殊字符`,
  })
  password: string
}

src/auth.controller.ts中引入dto参数校验对象

ts 复制代码
import { SignginUserDto } from './dto/singin-user.dto'

@Controller('auth')
@UseFilters(new TypeormFilter())
export class AuthController {
  constructor(private authService: AuthService) {}
  // 用户登录
  @Post('/signin')
  singin(@Body() body: SignginUserDto) {
    const { username = '', password = '' } = body
    return this.authService.singin(username, password)
  }
  // 用户注册
  @Post('/signup')
  signup(@Body() body: SignginUserDto) {
    const { username = '', password = '' } = body
    return this.authService.signup(username, password)
  }
}

请求接口提示如下

自定义正则验证参数

创建文件src/utils/validator.helper.ts

ts 复制代码
import { Matches } from 'class-validator'
/** 自定义登录密码验证(复杂版本,备用) */
export function CustomPasswordComplex() {
  return Matches(/^\S*(?=\S{6,20})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*?])\S*$/, {
    message: `密码不正确!长度6-20位,至少包含一个大写、小写字母、数字、特殊字符如(!@#$%^&*?)`,
  })
}

/** 自定义登录密码验证 */
export function CustomPassword() {
  return Matches(/^\S*(?=\S{6,20})(?=\S*\d)(?=\S*[A-Za-z])\S*$/, {
    message: `密码不正确!长度6-20位,至少包含一个字母、一个数字`,
  })
}

/** @IsNotEmpty({ message: `用户名不能为空` }) */
export function CustomUsername() {
  return Matches(/^([@.-_a-zA-Z0-9]{6,20})+$/, {
    message: `用户名不正确!长度6-20位,可以含字母,数字,下划线,减号`,
  })
}

src/auth.controller.ts中使用

ts 复制代码
import { IsNotEmpty } from 'class-validator'
import { CustomPassword, CustomUsername } from '../../utils/validator.helper'

export class SignginUserDto {
  @IsNotEmpty({ message: `用户名不能为空` })
  @CustomUsername()
  username: string

  @IsNotEmpty({ message: `密码不能为空` })
  @CustomPassword()
  password: string
}

内置管道, 自动转化类型

内置类型:ParseIntPipe,ParseUUIDPipe,ParseEnumPipe,ParseFloatPipe...

ts 复制代码
import { ParseIntPipe } from '@nestjs/common'
...
/** 获取用户详情  */
@Get('/profile/:id')
getUserProfile(@Param('id', ParseIntPipe) id: number) {
  console.log('====', typeof id) // ==== number
  return this.userService.findProfile(id)
}
...

自定义管道

使用nest命令行工具创建文件src/user/pipes/create-user.pipe.ts

sh 复制代码
nest g pi user/pipes/create-user -d
nest g pi user/pipes/create-user --no-spec
ts 复制代码
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'
import { CreateUserDto } from '../dto/create-user.dto'
import { Roles } from '../../roles/roles.entity'

@Injectable()
export class CreateUserPipe implements PipeTransform {
  transform(value: CreateUserDto, metadata: ArgumentMetadata) {
    // 此处可以对入参进行任何处理
    if (Array.isArray(value.roles) && value.roles.length > 0) {
      value.roles = value.roles.map((item: Roles | number) => {
        if (item['id']) return item['id']
        return item
      })
    }
    return value
  }
}

src/user/user.controller.ts中使用

ts 复制代码
import { CreateUserPipe } from './pipes/create-user.pipe'
...
/**
* 增加一个用户
* @param dto
* @returns
*/
@Post()
addUser(@Body(CreateUserPipe) dto: CreateUserDto) {
  const user = dto as User
  return this.userService.create(user)
}
...

创建一个文件src/user/dtos/create-user.dto.ts, 在上面使用这个dto

ts 复制代码
import { Roles } from 'src/roles/roles.entity'
import { CustomPassword, CustomUsername } from '../../utils/validator.helper'
import { IsNotEmpty, IsOptional } from 'class-validator'

export class CreateUserDto {
  @IsNotEmpty({ message: `用户名不能为空` })
  @CustomUsername()
  username: string

  @IsNotEmpty({ message: `密码不能为空` })
  @CustomPassword()
  password: string

  @IsOptional()
  roles: Roles[] | number[]

  @IsOptional()
  profile: { gender: 1 | 2; photo: string; address: string }
}

JWT登录鉴权

安装jwt相关插件

sh 复制代码
$ npm install --save @nestjs/jwt passport-jwt  
$ npm install --save-dev @types/passport-jwt
$ npm install --save passport @nestjs/passport

.env中添加JWT_SECRET密码配置

ini 复制代码
# jwt secret , 在线密码生成器:https://www.bchrt.com/tools/suijimima/
JWT_SECRET="GkzpYnZdtkf!7A^7eFAy5Ph$WDcMde4FDgt8$YeF@PEhkvxsgHKt#Gr3XT6"

编辑配置文件src/enum/config.enum.ts

ts 复制代码
export enum ConfigEnum {
  ...
  + JWT_SECRET = 'JWT_SECRET',
}

创建src/auth/jwt.strategy.ts

这个文件会获取接口请求头Authorization并进行解析并验证此token是否有效,并通过validate方法返回解析出来的用户信息。此信息将保存在request.user字段中

ts 复制代码
import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { ConfigEnum } from '../enum/config.enum'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(protected configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get(ConfigEnum.JWT_SECRET),
    })
  }
  async validate(payload: Record<string, any>) {
    return { userId: payload.sub, username: payload.username }
  }
}

编辑src/auth/auth.module.ts,配置JWT

ts 复制代码
import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'
import { UserModule } from '../user/user.module'
import { JwtModule } from '@nestjs/jwt'
import { PassportModule } from '@nestjs/passport'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { ConfigEnum } from '../enum/config.enum'
import { JwtStrategy } from './jwt.strategy'

@Module({
  imports: [
    UserModule,
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        return {
          // 密钥,可以在线密码生成:https://www.bchrt.com/tools/suijimima/
          secret: configService.get<string>(ConfigEnum.JWT_SECRET),
          signOptions: {
            /** 设置过期时间 [zeit/ms](https://github.com/zeit/ms.js).  Eg: 60, "2 days", "10h", "7d" */
            expiresIn: '1d',
          },
        }
      },
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

编辑src/auth/auth.controller.ts,修改登录接口

用户登录验证成功后会得到token和用户信息, 在前端页面把token保存在本地,以后每次接口请求应该把该token赋值给请求头的Authorization字段.

ts 复制代码
// 用户登录
@Post('/signin')
async singin(@Body() body: SignginUserDto) {
  const { username = '', password = '' } = body
  const { token, userInfo } = await this.authService.singin(username, password)
  // 登录成功后返回token和用户信息
  return { access_token: token, userInfo }
}

编辑src/auth/auth.service.ts

在这个service里面验证用户账号和密码,匹配成功后会生成token并返回

ts 复制代码
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { UserService } from '../user/user.service'
import { JwtService } from '@nestjs/jwt'

@Injectable()
export class AuthService {
  constructor(
    private userService: UserService,
    private jwt: JwtService,
  ) {}

  // 用户登录
  async singin(username: string, password: string) {
    const res = await this.userService.findAll({ username } as getUserDto)
    const user = res.total > 0 ? res.records[0] : null
    if (user && user.password === password) {
      const { password, ...userInfo } = user
      // 生成token
      const token = await this.jwt.signAsync({ username, sub: user.id })
      return { token, userInfo }
    }
    throw new UnauthorizedException()
  }
}

对用户请求的接口做权限校验

ts 复制代码
...
import { UseGuards,  Req, } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

@Controller('user')
@UseFilters(new TypeormFilter())
export class UserController {
  constructor(
    private userService: UserService,
  ) { }
  
  /** 获取用户详情 */
  @Get('/profile/:id')
  @UseGuards(AuthGuard('jwt')) // 这个配置会对用户做权限校验
  getUserProfile(@Param('id', ParseIntPipe) id: number, @Req() req) {
    // 这里req.user是通过AuthGuard('jwt')中的validate方法返回的,PassportModule来添加的
    console.log('hhah 用户注册', req.user)
    return this.userService.findProfile(id)
  }
}

守卫

nest命令行创建admin守卫

scss 复制代码
sh
$ nest g gu guards/admin --flat --no-spec -d # 查看将生成哪些文件(不需要测试文件)
$ nest g mo guards/admin --flat --no-spec # 生成文件"src/guards/jwt.guard.ts"(不需要测试文件)

编辑文件:src/guards/jwt.guard.ts

ts 复制代码
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { UserService } from '../user/user.service'

@Injectable()
export class AdminGuard implements CanActivate {
  constructor(private userService: UserService) {}
  
  // 返回结果为true:有权限, false:无权限
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // 1.获取用户信息
    const req = context.switchToHttp().getRequest()
    const user = await this.userService.findOne(req.user?.userId)
    // 当前用户拥有的角色id [{id:1, name:'管理员'},{id:2,name:'普通用户']
    const roleIds = user ? user.roles.map((r) => r.id) : []
    console.log('req.user', user, roleIds)
    // 当前用户是否存在管理员权限
    if (roleIds.includes(1)) return true
    return false
  }
}
  1. 在控制器使用守卫
ts 复制代码
@Controller('user')
@UseGuards(AuthGuard('jwt'), AdminGuard)
export class UserController {}
  1. 在控制器方法中使用守卫
ts 复制代码
// 使用方式1, 配置了多个守卫,守卫会从左到右依次执行
@Get()
@UseGuards(AuthGuard('jwt'), AdminGuard)
getList() {}

// 使用方式2, 配置了多个守卫,守卫会从下到上依次执行
@Get()
@UseGuards(AdminGuard)
@UseGuards(AuthGuard('jwt'))
getList() {}

权限工厂

创建文件src/guards/role.guard.ts

ts 复制代码
import { CanActivate, ExecutionContext } from '@nestjs/common'
import { UserService } from '../user/user.service'
/**
 * 这是一个返回权限守卫的工厂函数
 * @param priviIds 需要验证的权限id
 * @returns
 */
export function genRoleGuard(priviIds?: number | number[]) {
  return class RoleGuard implements CanActivate {
    constructor(public userService: UserService) {}
    // 返回结果为true:有权限, false:无权限
    async canActivate(context: ExecutionContext): Promise<boolean> {
      // 1.获取用户信息
      const req = context.switchToHttp().getRequest()
      const user = await this.userService.findOne(req.user?.userId)
      // 当前用户拥有的角色id
      const roleIds = user ? user.roles.map((r) => r.id) : []
      console.log('req.user', user, roleIds)
      // 当前用户是否存在管理员权限
      if (typeof priviIds === 'number' && roleIds.includes(priviIds)) return true
      if (Array.isArray(priviIds)) priviIds.forEach((pId: number) => roleIds.includes(pId))
      // 2.判断用户角色
      return false
    }
  }
}

优化文件:src/guards/admin.guard.ts, 继承字权限工厂函数genRoleGuard

scala 复制代码
import { Injectable } from '@nestjs/common'
import { UserService } from '../user/user.service'
import { genRoleGuard } from './role.guard'

/**
 * 管理员权限(1:管理员权限, 只有角色id===1的用户才有权限访问)
 */
@Injectable()
export class AdminGuard extends genRoleGuard(1) {
  constructor(public userService: UserService) {
    super(userService)
  }
}

优化AuthGuard的使用

创建守卫src/guards/jwt.guard.ts

ts 复制代码
import { AuthGuard } from '@nestjs/passport'

/** 用户登录权限守卫 */
export class JwtGuard extends AuthGuard('jwt') {
  constructor() {
    super()
  }
}

在控制器使用守卫

ts 复制代码
import { JwtGuard } from '../guards/jwt.guard'

@Controller('user')
+ @UseGuards(JwtGuard)
export class UserController {}
相关推荐
安冬的码畜日常44 分钟前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
DOKE3 小时前
VSCode终端:提升命令行使用体验
前端
xgq3 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081353 小时前
前端之路-了解原型和原型链
前端
永远不打烊3 小时前
librtmp 原生API做直播推流
前端