NestJS接入JWT用户验证的完整实现指南

NestJS接入JWT用户验证的完整实现指南

前言

作为一名前端团队的技术负责人,我负责设计并实现了基于NestJS和JWT的完整用户认证系统。这个方案应用于一个数据管理平台,需要严格的用户权限控制和安全认证机制。在这篇文章中,我将分享如何在NestJS后端实现JWT认证,以及前端如何配合这一机制进行用户验证的完整实践经验。

JWT简介

JWT(JSON Web Token)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象安全地传输信息。这种信息可以被验证和信任,因为它是数字签名的。

JWT由三部分组成,以点(.)分隔:

  • 头部(Header)
  • 载荷(Payload)
  • 签名(Signature)

相比传统的基于session的认证方式,JWT具有以下优势:

  • 跨域友好:由于JWT是自包含的,可以轻松在不同域间传递
  • 无状态:服务器不需要保存session状态
  • 可扩展性好:更容易进行水平扩展

项目需求与架构

在我们的项目中,认证系统需要满足以下要求:

  • 用户登录并获取JWT令牌
  • 令牌包含用户ID、角色等信息
  • 受保护的API路由需要验证令牌
  • 令牌有合理的过期时间,支持刷新机制
  • 前端需要自动处理令牌存储和刷新

基于这些需求,我们设计了以下架构:

  • 后端:NestJS框架 + TypeORM + MySQL
  • 前端:Vue 3 + Axios + Pinia
  • 认证:JWT + Guards + Interceptors

后端实现

1. 安装必要依赖

首先,我们需要安装以下依赖包:

1
2
npm install @nestjs/jwt passport passport-jwt @nestjs/passport bcrypt
npm install -D @types/passport-jwt @types/bcrypt

2. 创建Auth模块

在NestJS中,我们通常将认证相关功能封装在一个独立的模块中。

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
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1h'),
},
}),
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}

3. 实现认证服务

认证服务负责处理用户验证、令牌生成和验证等核心功能。

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
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}

async validateUser(username: string, password: string): Promise<any> {
const user = await this.usersService.findByUsername(username);

if (user && await bcrypt.compare(password, user.password)) {
// 不返回密码
const { password, ...result } = user;
return result;
}
return null;
}

async login(user: any) {
// 创建JWT Payload
const payload = {
sub: user.id,
username: user.username,
roles: user.roles
};

// 生成访问令牌
const accessToken = this.jwtService.sign(payload);

// 生成刷新令牌(有效期更长)
const refreshToken = this.jwtService.sign(
{ sub: user.id },
{ expiresIn: '7d' }
);

return {
access_token: accessToken,
refresh_token: refreshToken,
user: {
id: user.id,
username: user.username,
email: user.email,
roles: user.roles,
}
};
}

async refreshToken(refreshToken: string) {
try {
// 验证刷新令牌
const payload = this.jwtService.verify(refreshToken);
const user = await this.usersService.findOne(payload.sub);

if (!user) {
throw new UnauthorizedException('Invalid user');
}

// 生成新的访问令牌
const newPayload = {
sub: user.id,
username: user.username,
roles: user.roles
};

return {
access_token: this.jwtService.sign(newPayload),
};
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
}

4. 创建JWT策略

JWT策略定义了如何从请求中提取和验证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
27
// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}

async validate(payload: any) {
// 该方法将被Passport调用,返回值会被注入到请求对象中
return {
id: payload.sub,
username: payload.username,
roles: payload.roles
};
}
}

5. 实现Auth控制器

控制器负责处理登录、令牌刷新等HTTP请求。

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
// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, HttpCode, Request, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';

@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}

@Post('login')
@HttpCode(200)
async login(@Body() loginDto: { username: string; password: string }) {
const user = await this.authService.validateUser(
loginDto.username,
loginDto.password,
);

if (!user) {
return { success: false, message: '用户名或密码错误' };
}

return this.authService.login(user);
}

@Post('refresh')
@HttpCode(200)
async refresh(@Body() body: { refresh_token: string }) {
return this.authService.refreshToken(body.refresh_token);
}

@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}

6. 创建守卫

守卫用于保护路由,确保只有经过认证的用户才能访问受保护的资源。

1
2
3
4
5
6
// src/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

7. 实现角色授权

除了基本认证外,我们还需要基于角色的授权。

1
2
3
4
5
// src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);

if (!requiredRoles) {
return true;
}

const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}

8. 在其他模块中使用JWT认证

现在,我们可以在任何需要保护的路由中使用JWT认证和角色授权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { UsersService } from './users.service';

@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
constructor(private usersService: UsersService) {}

@Get()
@Roles('admin')
findAll() {
return this.usersService.findAll();
}
}

前端实现

1. 创建API服务

首先,我们需要创建一个API服务来处理与后端的通信:

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
95
96
97
98
99
100
101
102
103
104
105
106
107
// src/services/api.service.ts
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
import { useAuthStore } from '@/stores/auth';

export class ApiService {
private api: AxiosInstance;
private static instance: ApiService;

private constructor() {
this.api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});

this.setupInterceptors();
}

public static getInstance(): ApiService {
if (!ApiService.instance) {
ApiService.instance = new ApiService();
}

return ApiService.instance;
}

private setupInterceptors(): void {
// 请求拦截器
this.api.interceptors.request.use(
(config) => {
const authStore = useAuthStore();
const token = authStore.accessToken;

if (token) {
config.headers.Authorization = `Bearer ${token}`;
}

return config;
},
(error) => Promise.reject(error)
);

// 响应拦截器
this.api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const authStore = useAuthStore();
const originalRequest = error.config;

// 如果是401错误且不是刷新令牌请求,尝试刷新令牌
if (error.response?.status === 401 &&
originalRequest &&
!(originalRequest.url?.includes('auth/refresh'))) {

// 已经尝试过刷新令牌但仍失败,则登出
if (originalRequest.headers['X-Retry-After-Refresh']) {
authStore.logout();
return Promise.reject(error);
}

try {
// 尝试刷新令牌
await authStore.refreshToken();

// 更新原始请求中的授权头
originalRequest.headers['Authorization'] = `Bearer ${authStore.accessToken}`;
originalRequest.headers['X-Retry-After-Refresh'] = 'true';

// 重试原始请求
return this.api(originalRequest);
} catch (refreshError) {
// 刷新令牌失败,登出
authStore.logout();
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);
}

// API方法
public async get<T = any>(url: string, params = {}): Promise<T> {
const response: AxiosResponse<T> = await this.api.get(url, { params });
return response.data;
}

public async post<T = any>(url: string, data = {}): Promise<T> {
const response: AxiosResponse<T> = await this.api.post(url, data);
return response.data;
}

public async put<T = any>(url: string, data = {}): Promise<T> {
const response: AxiosResponse<T> = await this.api.put(url, data);
return response.data;
}

public async delete<T = any>(url: string): Promise<T> {
const response: AxiosResponse<T> = await this.api.delete(url);
return response.data;
}
}

export const apiService = ApiService.getInstance();

2. 创建认证Store

使用Pinia管理认证状态:

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// src/stores/auth.ts
import { defineStore } from 'pinia';
import { apiService } from '@/services/api.service';
import router from '@/router';

interface User {
id: number;
username: string;
email: string;
roles: string[];
}

interface AuthState {
user: User | null;
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
}

export const useAuthStore = defineStore('auth', {
state: (): AuthState => ({
user: null,
accessToken: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken'),
isAuthenticated: !!localStorage.getItem('accessToken'),
}),

getters: {
isAdmin: (state) => state.user?.roles.includes('admin') ?? false,
},

actions: {
async login(username: string, password: string) {
try {
const response = await apiService.post<{
access_token: string;
refresh_token: string;
user: User;
}>('auth/login', { username, password });

this.setAuthData(
response.access_token,
response.refresh_token,
response.user
);

return response;
} catch (error) {
console.error('Login failed:', error);
throw error;
}
},

async refreshToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}

try {
const response = await apiService.post<{ access_token: string }>(
'auth/refresh',
{ refresh_token: this.refreshToken }
);

this.accessToken = response.access_token;
localStorage.setItem('accessToken', response.access_token);

return response;
} catch (error) {
console.error('Token refresh failed:', error);
this.logout();
throw error;
}
},

logout() {
this.user = null;
this.accessToken = null;
this.refreshToken = null;
this.isAuthenticated = false;

localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');

router.push('/login');
},

setAuthData(accessToken: string, refreshToken: string, user: User) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.user = user;
this.isAuthenticated = true;

localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
},

async fetchUserProfile() {
if (!this.isAuthenticated) {
return null;
}

try {
const user = await apiService.get<User>('auth/profile');
this.user = user;
return user;
} catch (error) {
console.error('Failed to fetch user profile:', error);
throw error;
}
},
},
});

3. 创建路由守卫

使用Vue Router的导航守卫来保护需要认证的路由:

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
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { useAuthStore } from '@/stores/auth';

const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true },
},
{
path: 'admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, roles: ['admin'] },
},
],
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { guestOnly: true },
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
},
];

const router = createRouter({
history: createWebHistory(),
routes,
});

router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore();

// 如果有token但没有用户信息,尝试获取用户信息
if (authStore.isAuthenticated && !authStore.user) {
try {
await authStore.fetchUserProfile();
} catch (error) {
authStore.logout();
return next('/login');
}
}

// 处理需要认证的路由
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return next('/login');
}

// 处理需要特定角色的路由
if (to.meta.roles && authStore.isAuthenticated) {
const hasRequiredRole = to.meta.roles.some((role: string) =>
authStore.user?.roles.includes(role)
);

if (!hasRequiredRole) {
return next('/');
}
}

// 已登录用户不应访问登录页
if (to.meta.guestOnly && authStore.isAuthenticated) {
return next('/');
}

next();
});

export default router;

4. 登录组件实现

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<!-- src/views/Login.vue -->
<template>
<div class="login-container">
<h1>登录</h1>
<div v-if="error" class="error-message">{{ error }}</div>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label for="username">用户名</label>
<input
id="username"
v-model="username"
type="text"
required
autocomplete="username"
/>
</div>

<div class="form-group">
<label for="password">密码</label>
<input
id="password"
v-model="password"
type="password"
required
autocomplete="current-password"
/>
</div>

<button type="submit" :disabled="isLoading">
{{ isLoading ? '登录中...' : '登录' }}
</button>
</form>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '@/stores/auth';

const router = useRouter();
const authStore = useAuthStore();

const username = ref('');
const password = ref('');
const error = ref('');
const isLoading = ref(false);

const handleLogin = async () => {
error.value = '';
isLoading.value = true;

try {
await authStore.login(username.value, password.value);
router.push('/dashboard');
} catch (err: any) {
if (err.response?.data?.message) {
error.value = err.response.data.message;
} else {
error.value = '登录失败,请稍后再试';
}
} finally {
isLoading.value = false;
}
};
</script>

<style scoped>
.login-container {
max-width: 400px;
margin: 0 auto;
padding: 2rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

.form-group {
margin-bottom: 1rem;
}

label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}

input {
width: 100%;
padding: 0.5rem;
border: 1px solid #e2e8f0;
border-radius: 0.25rem;
}

button {
width: 100%;
padding: 0.5rem;
background-color: #4f46e5;
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}

button:disabled {
opacity: 0.7;
cursor: not-allowed;
}

.error-message {
color: #dc2626;
margin-bottom: 1rem;
padding: 0.5rem;
background-color: #fee2e2;
border-radius: 0.25rem;
}
</style>

完整实例:用户管理功能

让我们通过一个完整的用户管理功能来展示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
// src/users/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, BeforeInsert } from 'typeorm';
import * as bcrypt from 'bcrypt';

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column({ unique: true })
username: string;

@Column()
password: string;

@Column({ unique: true })
email: string;

@Column('simple-array')
roles: string[];

@BeforeInsert()
async hashPassword() {
this.password = await bcrypt.hash(this.password, 10);
}
}
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
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

async findAll(): Promise<User[]> {
return this.usersRepository.find();
}

async findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}

async findByUsername(username: string): Promise<User | null> {
return this.usersRepository.findOneBy({ username });
}

async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}

async remove(id: number): Promise<void> {
await this.usersRepository.delete(id);
}
}

前端用户管理组件

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!-- src/views/Admin.vue -->
<template>
<div class="admin-container">
<h1>用户管理</h1>

<div v-if="isLoading" class="loading">加载中...</div>

<table v-else class="users-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.roles.join(', ') }}</td>
<td>
<button
v-if="user.id !== currentUser?.id"
@click="deleteUser(user.id)"
class="delete-button"
>
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { apiService } from '@/services/api.service';

interface User {
id: number;
username: string;
email: string;
roles: string[];
}

const authStore = useAuthStore();
const users = ref<User[]>([]);
const isLoading = ref(true);

const currentUser = computed(() => authStore.user);

onMounted(async () => {
try {
const response = await apiService.get<User[]>('users');
users.value = response;
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
isLoading.value = false;
}
});

const deleteUser = async (userId: number) => {
if (!confirm('确定要删除此用户吗?')) {
return;
}

try {
await apiService.delete(`users/${userId}`);
users.value = users.value.filter(user => user.id !== userId);
} catch (error) {
console.error('Failed to delete user:', error);
}
};
</script>

<style scoped>
.admin-container {
padding: 1rem;
}

.loading {
text-align: center;
padding: 2rem;
font-style: italic;
color: #666;
}

.users-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}

.users-table th,
.users-table td {
border: 1px solid #e2e8f0;
padding: 0.75rem;
text-align: left;
}

.users-table th {
background-color: #f8fafc;
font-weight: 600;
}

.users-table tr:nth-child(even) {
background-color: #f8fafc;
}

.delete-button {
background-color: #ef4444;
color: white;
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
cursor: pointer;
}

.delete-button:hover {
background-color: #dc2626;
}
</style>

安全最佳实践

在实际项目中,我们还采取了以下安全措施来加强JWT认证系统:

  1. HTTPS: 所有API通信都通过HTTPS进行,防止中间人攻击。

  2. 令牌过期时间: 访问令牌设置较短的过期时间(如1小时),刷新令牌设置较长但有限的过期时间(如7天)。

  3. 黑名单机制: 当用户登出时,将其当前的访问令牌和刷新令牌加入黑名单,防止令牌被滥用。

  4. CORS设置: 正确配置跨域资源共享,只允许受信任的域名访问API。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // main.ts
    app.enableCors({
    origin: [
    'https://your-frontend-domain.com',
    /^https:\/\/.*\.your-frontend-domain\.com$/,
    ],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    credentials: true,
    });
  5. 请求限流: 防止暴力破解攻击。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // src/common/middleware/rate-limiter.middleware.ts
    import { Injectable, NestMiddleware } from '@nestjs/common';
    import { Request, Response, NextFunction } from 'express';
    import * as rateLimit from 'express-rate-limit';

    @Injectable()
    export class RateLimiterMiddleware implements NestMiddleware {
    private limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 100, // 每个IP最多100次请求
    message: '请求过于频繁,请稍后再试',
    });

    use(req: Request, res: Response, next: NextFunction) {
    this.limiter(req, res, next);
    }
    }
  6. 敏感数据保护: 确保密码等敏感数据在任何情况下都不会暴露给客户端。

总结与心得

在这个项目中实现NestJS与JWT的集成是一个非常有价值的经验。通过这个实践,我学到了:

  1. 模块化设计的重要性: NestJS的模块系统使得我们可以将认证逻辑与业务逻辑清晰分离。

  2. JWT的优势与局限: JWT提供了无状态认证的便利,但也需要注意刷新机制和令牌安全存储。

  3. 前后端协作: 良好的前后端认证协议设计使得系统更加稳定和安全。

  4. 用户体验: 透明的令牌刷新机制让用户不会因为认证过期而频繁登出。

作为团队的技术负责人,我认为安全和用户体验同样重要。在实现认证系统时,我们既要确保系统的安全性,又要保证用户使用过程的流畅性。

希望这篇文章能对你在NestJS项目中实现JWT认证有所帮助。如果你有任何问题或建议,欢迎在评论区留言讨论。

参考资料

  1. NestJS官方文档 - Authentication
  2. JWT官方网站
  3. Passport.js文档
  4. TypeORM文档
  5. Vue文档 - 路由
  6. Pinia文档

本文永久链接: https://www.mulianju.com/2025/nestjs-integration-with-jwt-authentication/