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
| 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
| 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) { 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
| 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) { 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
| 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
| import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport';
@Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {}
|
7. 实现角色授权
除了基本认证外,我们还需要基于角色的授权。
1 2 3 4 5
| 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
| 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
| 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
| 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; 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); } ); }
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
| 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
| 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(); 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
| 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
| 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认证系统:
HTTPS: 所有API通信都通过HTTPS进行,防止中间人攻击。
令牌过期时间: 访问令牌设置较短的过期时间(如1小时),刷新令牌设置较长但有限的过期时间(如7天)。
黑名单机制: 当用户登出时,将其当前的访问令牌和刷新令牌加入黑名单,防止令牌被滥用。
CORS设置: 正确配置跨域资源共享,只允许受信任的域名访问API。
1 2 3 4 5 6 7 8 9
| app.enableCors({ origin: [ 'https://your-frontend-domain.com', /^https:\/\/.*\.your-frontend-domain\.com$/, ], methods: ['GET', 'POST', 'PUT', 'DELETE'], credentials: true, });
|
请求限流: 防止暴力破解攻击。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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, max: 100, message: '请求过于频繁,请稍后再试', });
use(req: Request, res: Response, next: NextFunction) { this.limiter(req, res, next); } }
|
敏感数据保护: 确保密码等敏感数据在任何情况下都不会暴露给客户端。
总结与心得
在这个项目中实现NestJS与JWT的集成是一个非常有价值的经验。通过这个实践,我学到了:
模块化设计的重要性: NestJS的模块系统使得我们可以将认证逻辑与业务逻辑清晰分离。
JWT的优势与局限: JWT提供了无状态认证的便利,但也需要注意刷新机制和令牌安全存储。
前后端协作: 良好的前后端认证协议设计使得系统更加稳定和安全。
用户体验: 透明的令牌刷新机制让用户不会因为认证过期而频繁登出。
作为团队的技术负责人,我认为安全和用户体验同样重要。在实现认证系统时,我们既要确保系统的安全性,又要保证用户使用过程的流畅性。
希望这篇文章能对你在NestJS项目中实现JWT认证有所帮助。如果你有任何问题或建议,欢迎在评论区留言讨论。
参考资料
- NestJS官方文档 - Authentication
- JWT官方网站
- Passport.js文档
- TypeORM文档
- Vue文档 - 路由
- Pinia文档
本文永久链接: https://www.mulianju.com/2025/nestjs-integration-with-jwt-authentication/