nestjs的jwt授权认证

概述

最近在用nestjs重新做RBAC构架。
本次完整实践nestjs的注册登陆,到jwt授权认证,到guard守卫拦截验证,到strategy的JWT策略。
和Spring Boot的Shiro对比下来,还是nestjs的写着舒服。

实践

auth.controller.ts
注册登陆的controller实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Body, Controller, Post } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Result } from 'src/common/utils/result';
import { CreateUserDto } from '../user/dto/create.dto';
import { LoginUserDto } from '../user/dto/login.dto';
import { UserEntity } from '../user/user.entity';
import { UserService } from '../user/user.service';
import { AuthService } from './auth.service';

@ApiTags('登录注册')
@Controller('v1/auth')
export class AuthController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService
) { }

@Post('register')
@ApiOperation({ summary: '用户注册' })
@ApiOkResponse({ type: UserEntity })
async create(@Body() user: CreateUserDto): Promise<Result> {
console.log('user', user)
const res = await this.userService.create(user)
return Result.ok(res)
}

@Post('login')
@ApiOperation({ summary: '登录' })
async login(@Body() dto: LoginUserDto): Promise<Result> {
const res = await this.userService.login(dto.account, dto.password)
return Result.ok(res)
}
}

user.service.ts
注册登陆及JWT的service实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Like, Repository, UpdateResult } from 'typeorm';
import { UserEntity } from './user.entity';
import { Result } from 'src/common/utils/result';
import { CreateUserDto } from './dto/create.dto';
import { QueryUserDto } from './dto/query.dto';
import { classToPlain, plainToClass } from 'class-transformer';
import { RedisService } from 'nestjs-redis';
import { ConfigService } from '@nestjs/config';
import { genSalt, hash, compare } from 'bcrypt'
import { JwtService } from '@nestjs/jwt';
import { UpdateUserDto } from './dto/update.dto';
@Injectable()
export class UserService {
constructor(
@InjectRepository(UserEntity)
private readonly userRep: Repository<UserEntity>,
private readonly redisService: RedisService,
private readonly config: ConfigService,
private readonly jwtService: JwtService,
) { }

async create(dto: CreateUserDto): Promise<UserEntity | Result> {
console.log(dto)
const existing = await this.findByUsername(dto.username)
if (existing) throw new HttpException('账号已存在,请调整后重新注册!', HttpStatus.NOT_ACCEPTABLE);
const salt = await genSalt()
dto.password = await hash(dto.password, salt)
const user = plainToClass(UserEntity, { salt, ...dto }, { ignoreDecorators: true })
console.log('user', user)
const res = await this.userRep.save(user)
return res
}

// 登录
async login(account: string, password: string): Promise<object | Result> {
const user = await this.findByUsername(account)
console.log("user", user)
if (!user) throw new HttpException('账号或密码错误', HttpStatus.NOT_FOUND);
console.log('账号', account)
console.log('密码', password)
console.log('加密的密码', user.password)
const checkPassword = await compare(password, user.password)
console.log('是否一致', checkPassword)
if (!checkPassword) throw new HttpException('账号或密码错误', HttpStatus.NOT_FOUND);
// 生成 token
const data = this.genToken({ id: user.id })
return data
}

// 根据ID查找
async findById(id: number): Promise<UserEntity> {
const res = await this.userRep.findOne(id)
if (!res) {
throw new NotFoundException()
}
return res
}

// 根据用户名查找
async findByUsername(username: string): Promise<UserEntity> {
return await this.userRep.findOne({ username })
}

// 生成 token
genToken(payload: { id: number }): Record<string, unknown> {
const accessToken = `Bearer ${this.jwtService.sign(payload)}`
const refreshToken = this.jwtService.sign(payload, { expiresIn: this.config.get('jwt.refreshExpiresIn') })
return { accessToken, refreshToken }
}

// 刷新 token
refreshToken(id: number): string {
return this.jwtService.sign({ id })
}

// 校验 token
verifyToken(token: string): number {
try {
if (!token) return 0
const id = this.jwtService.verify(token.replace('Bearer ', ''))
return id
} catch (error) {
return 0
}
}

// 根据JWT解析的ID校验用户
async validateUserByJwt(payload: { id: number }): Promise<UserEntity> {
return await this.findById(payload.id)
}
}

jwt-auth.guard.ts
守卫实现,拦截accessToken和refreshToken,过期进行续签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
import { UserService } from 'src/modules/user/user.service';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly userService: UserService,
) {
super()
}
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
const req = context.switchToHttp().getRequest()
const res = context.switchToHttp().getResponse()
try {
const accessToken = req.get('Authorization')
if (!accessToken) throw new UnauthorizedException('请先登录')

const atUserId = this.userService.verifyToken(accessToken)
if (atUserId) return this.activate(context)
console.log(req.user)
const refreshToken = req.get('RefreshToken')
const rtUserId = this.userService.verifyToken(refreshToken)
if (!rtUserId) throw new UnauthorizedException('当前登录已过期,请重新登录')
const user = await this.userService.findById(rtUserId)
if (user) {
const tokens = this.userService.genToken({ id: rtUserId })
// request headers 对象 prop 属性全自动转成小写,
// 所以 获取 request.headers['authorization'] 或 request.get('Authorization')
// 重置属性 request.headers[authorization] = value
req.headers['authorization'] = tokens.accessToken
req.headers['refreshtoken'] = tokens.refreshToken
// 在响应头中加入新的token,客户端判断响应头有无 Authorization 字段,有则重置
res.header('Authorization', tokens.accessToken)
res.header('RefreshToken', tokens.refreshToken)
// 将当前请求交给下一级
return this.activate(context)
} else {
throw new UnauthorizedException('用户不存在')
}
} catch (error) {
// Logger
return false
}
}

async activate(context: ExecutionContext): Promise<boolean> {
return super.canActivate(context) as Promise<boolean>
}
}

jwt.strategy.ts
JWT解析后进行校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from 'src/modules/user/user.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly userService: UserService,
private readonly config: ConfigService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('jwt.secretkey'),
})
}

async validate(payload: any) {
const user = await this.userService.validateUserByJwt(payload)
// 如果有用户信息,代表 token 没有过期,没有则 token 已失效
if (!user) throw new UnauthorizedException()
return user
}
}