1. 安装依赖并配置 JWT

首先,安装@nestjs/jwt包:

1
pnpm add @nestjs/jwt -S

然后在.env文件中配置一下JWT的密钥与过期时间

1
2
3
# JWT配置
JWT_SECRET = bacdefg
JWT_EXP = 2h

接着,在app.module.ts中导入JwtModule并做配置:

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
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { UserModule } from './user/user.module'
import { TypeOrmModule } from '@nestjs/typeorm'
import { ConfigModule } from '@nestjs/config'
import { JwtModule } from '@nestjs/jwt'
import { UserGuard } from './user/user.guard'

@Module({
imports: [TypeOrmModule.forRoot({
type: 'mysql', // 数据库类型
host: 'localhost', // 数据库主机
port: 3306, // 数据库端口
username: 'root', // 数据库用户名
password: '123456', // 数据库密码
database: 'nest_demo', // 数据库名称
entities: [__dirname + '/**/*.entity{.ts,.js}'], // 数据库实体文件
synchronize: true, // 是否自动同步实体
// logging: true, // 是否打印日志
retryDelay: 500, // 重试连接间隔
retryAttempts: 10, // 重试连接次数
autoLoadEntities: true, // 自动加载实体

}), ConfigModule.forRoot({
// 加载特定环境的 .env 文件
envFilePath: `.env`,
// 使配置服务在所有模块中可用
isGlobal: true,
}), JwtModule.registerAsync({
global:true, // 是否全局注册模块
useFactory: async () => {
return {
secret: process.env.JWT_SECRET, // 密钥
signOptions: { expiresIn: process.env.JWT_EXP }, // 过期时间
}
}
}), UserModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }

2. 实现登录功能

user.service.ts中实现用户登录,校验用户信息并生成JWT token

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
import { User } from './entities/user.entity';
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { hash, verify } from '../utils/md5'
import { JwtService } from '@nestjs/jwt'

@Injectable()
export class UserService {
// 注释:这里使用@InjectRepository 注解,注入User实体,使用Repository<User>
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private jwtService: JwtService,
) { }

// 登录
async login(loginDto: CreateUserDto) {
const { username, password } = loginDto
const user = await this.findByUsername(username)
if (!user) return '用户不存在'
if (!verify(password, user.password, process.env.MD5_SALT)) {
return '密码错误'
}
const payload = { username: user.username, id: user.id };
return await this.jwtService.signAsync(payload); // 生成token
}

// 根据用户名查询用户
async findByUsername(username: string) {
// const user = await this.usersRepository.findOneBy({ username })
const user = await this.usersRepository.findOne({
where: { username },
select: ['id', 'username', 'password']
})
return user
}
}

登录成功后,返回的token如下:

1
2
3
4
5
6
{
"code": 200,
"success": "请求成功",
"timestamp": "2024/9/15 23:38:31",
"data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

3. 导航守卫 Guard 实现

导航守卫用于拦截路由导航,检查用户是否已登录。

通过nest g gu user生成守卫user.guard.ts
规定前端请求头字段为authorization,并且以Bearer ${token}(约定俗称)这种形式传值,将guard改为

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
import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Reflector } from '@nestjs/core'

@Injectable()
export class UserGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
) { }

/**
* 判断请求是否通过身份验证
* @param context 执行上下文
* @returns 是否通过身份验证
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest(); // 获取请求对象
const token = this.extractTokenFromHeader(request); // 从请求头中提取token
if (!token) {
throw new HttpException('验证不通过', HttpStatus.FORBIDDEN); // 如果没有token,抛出验证不通过异常
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET, // 使用JWT_SECRET解析token
});
request['user'] = payload; // 将解析后的用户信息存储在请求对象中
} catch {
throw new HttpException('token验证失败', HttpStatus.FORBIDDEN); // token验证失败,抛出异常
}

return true; // 身份验证通过
}

/**
* 从请求头中提取token
* @param request 请求对象
* @returns 提取到的token
*/
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = (request.headers as { authorization?: string }).authorization?.split(' ') ?? []; // 从Authorization头中提取token
return type === 'Bearer' ? token : undefined; // 如果是Bearer类型的token,返回token;否则返回undefined
}
}

测试守卫

user.module.ts中将UserGuard配置为全局守卫:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { UserGuard } from './user.guard';
@Controller('user')
export class AuthController {
constructor(private readonly authService: AuthService) {}

//省略部分代码
@UseGuards(UserGuard)
@Post('test')
test() {
return 1;
}
}

请求结果:

1
2
3
4
5
6
7
{
"success": false,
"timestamp": "2024/9/15 23:54:33",
"message": "验证不通过",
"code": 403,
"path": "/api/v1/user/3"
}

配置全局守卫

现在我们注册成全局守卫,在user.module.ts引入APP_GUARD并将UserGuard注入,这样它就成为了全局守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//省略部分代码
import { APP_GUARD } from "@nestjs/core";
import { UserGuard } from "./user.guard";
@Module({
controllers: [UserController],
providers: [
UserService,
{
provide: APP_GUARD,
useClass: UserGuard,
},
],
imports: [TypeOrmModule.forFeature([User]), CacheModule],
})
export class UserModule {}

自定义装饰器:免登录访问

这时候将user.controller.ts中的装饰器@UseGuards去掉
发现守卫依然有效
在业务中并不是所有接口都需要验证,比如登录接口,我们可以通过自定义装饰器设置元数据来处理
执行nest g d public生成一个public.decorator.ts创建一个装饰器设置一下元数据isPublictrue

1
2
3
import { SetMetadata } from '@nestjs/common'

export const Public = () => SetMetadata('isPublic', true)

然后在user.guard.ts中通过Reflector取出当前的isPublic,如果为true(即用了@Public进行装饰过),则不再进行token判断直接放行

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
import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Reflector } from '@nestjs/core'

@Injectable()
export class UserGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
) { }

/**
* 判断请求是否通过身份验证
* @param context 执行上下文
* @returns 是否通过身份验证
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
//即将调用的方法
context.getHandler(),
//controller类型
context.getClass(),
])
if (isPublic) {
return true;
}
// 省略其他代码
}

在不需要验证的接口上添加@Public()装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { Public } from './public/public.decorator';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Public()
@Get()
getHello(): string {
return this.appService.getHello();
}
}

返回结果如下

1
2
3
4
5
6
{
"code": 200,
"success": "请求成功",
"timestamp": "2024/9/16 00:04:17",
"data": "Hello World!"
}

4. 获取当前用户信息

执行nest g d user生成user.decorator.ts,实现一个用于获取当前用户信息的装饰器:

1
2
3
4
5
6
7
8
9
import { createParamDecorator, ExecutionContext, SetMetadata } from '@nestjs/common';

/**
* 获取当前用户信息
* @param data 获取用户信息中的字段 key 值 例如:id username
*/
export const User = createParamDecorator((data: string, ctx: ExecutionContext) => {
return ctx.switchToHttp().getRequest().user[data];
})

user.controller.ts中添加接口获取用户信息:

1
2
3
4
5
6
7
8
9
10
11
import { Controller, Get, UseGuards } from '@nestjs/common'
import { User } from './user.decorator';
@Controller('user')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Get('test')
test(@User('username') username: string) {
return username; // 返回用户名
}
}

总结

本文介绍了如何在 NestJS 中通过 JwtModule 配置 JWT 认证,使用全局守卫拦截路由,并自定义装饰器获取用户信息或处理免登录路由。通过这种方式,我们可以很方便地实现用户认证和路由保护,确保应用的安全性和灵活性。