登录鉴权方案对比
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
}
}
- 在控制器使用守卫
ts
@Controller('user')
@UseGuards(AuthGuard('jwt'), AdminGuard)
export class UserController {}
- 在控制器方法中使用守卫
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 {}