Vue 3 与 JWT 深度集成
概述
JSON Web Token (JWT) 是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明。在 Vue 3 应用中,JWT 常用于用户认证和授权,特别是在前后端分离架构中。本教程将深入探讨 Vue 3 与 JWT 的深度集成,包括 JWT 的生成、验证、存储和刷新等高级主题。
核心知识
1. JWT 基本结构
JWT 由三部分组成,用点(.)分隔:
- Header:包含算法和令牌类型
- Payload:包含声明(claims)
- Signature:用于验证令牌的完整性
2. JWT 认证流程
- 用户使用凭证登录
- 服务器验证凭证并生成 JWT
- 服务器将 JWT 返回给客户端
- 客户端存储 JWT(通常在 localStorage 或 Cookie 中)
- 客户端在后续请求中携带 JWT
- 服务器验证 JWT 并处理请求
3. Vue 3 中 JWT 的状态管理
在 Vue 3 中,我们可以使用 Composition API 和 Pinia 来管理 JWT 状态:
// src/stores/auth.js
import { defineStore } from 'pinia'
import { useJWT } from '../composables/useJWT'
export const useAuthStore = defineStore('auth', () => {
const { decodeToken, validateToken, refreshToken } = useJWT()
const token = ref(localStorage.getItem('token') || null)
const user = ref(null)
const isAuthenticated = computed(() => {
return token.value && validateToken(token.value)
})
const login = async (credentials) => {
// 调用登录 API 获取 token
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
headers: {
'Content-Type': 'application/json'
}
})
const data = await response.json()
token.value = data.token
user.value = decodeToken(data.token)
localStorage.setItem('token', data.token)
}
const logout = () => {
token.value = null
user.value = null
localStorage.removeItem('token')
}
const refresh = async () => {
const newToken = await refreshToken(token.value)
if (newToken) {
token.value = newToken
user.value = decodeToken(newToken)
localStorage.setItem('token', newToken)
} else {
logout()
}
}
return {
token,
user,
isAuthenticated,
login,
logout,
refresh
}
})4. JWT 组合式函数
创建一个 useJWT 组合式函数来处理 JWT 的核心逻辑:
// src/composables/useJWT.js
import { ref } from 'vue'
export function useJWT() {
const isLoading = ref(false)
const error = ref(null)
// 解码 JWT
const decodeToken = (token) => {
try {
const base64Url = token.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
window.atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
return JSON.parse(jsonPayload)
} catch (err) {
error.value = 'Invalid token'
return null
}
}
// 验证 JWT 是否过期
const validateToken = (token) => {
const decoded = decodeToken(token)
if (!decoded || !decoded.exp) return false
const currentTime = Math.floor(Date.now() / 1000)
return decoded.exp > currentTime
}
// 刷新 JWT
const refreshToken = async (token) => {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/refresh-token', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error('Failed to refresh token')
}
const data = await response.json()
return data.token
} catch (err) {
error.value = err.message
return null
} finally {
isLoading.value = false
}
}
// 从请求头中提取 JWT
const extractTokenFromHeader = (headers) => {
const authHeader = headers.get('Authorization')
if (!authHeader) return null
const [bearer, token] = authHeader.split(' ')
return bearer === 'Bearer' ? token : null
}
return {
isLoading,
error,
decodeToken,
validateToken,
refreshToken,
extractTokenFromHeader
}
}5. 路由守卫与 JWT
在 Vue Router 中使用路由守卫来保护需要认证的路由:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue'),
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
if (requiresAuth && !authStore.isAuthenticated) {
next('/login')
} else if (requiresAuth && authStore.token) {
// 检查 token 是否即将过期
const decoded = authStore.decodeToken(authStore.token)
const currentTime = Math.floor(Date.now() / 1000)
// 如果 token 将在 5 分钟内过期,刷新 token
if (decoded.exp - currentTime < 300) {
await authStore.refresh()
}
next()
} else {
next()
}
})
export default router最佳实践
1. JWT 存储安全
- 避免使用 localStorage:localStorage 易受 XSS 攻击,建议使用 HttpOnly Cookie
- 使用 secure 和 HttpOnly 标志:确保 Cookie 只通过 HTTPS 传输,且无法被 JavaScript 访问
- 设置合理的过期时间:短期访问令牌(Access Token)+ 长期刷新令牌(Refresh Token)
2. JWT 刷新机制
- 实现自动刷新:在令牌过期前自动刷新
- 处理刷新失败:刷新失败时重定向到登录页面
- 限制刷新次数:防止滥用刷新令牌
3. JWT 验证
- 总是在服务器端验证:客户端验证只能提高用户体验,不能替代服务器端验证
- 验证签名和过期时间:确保令牌未被篡改且未过期
- 验证颁发者和受众:确保令牌是为你的应用颁发的
4. 错误处理
- 处理无效令牌:显示合适的错误信息
- 处理过期令牌:自动刷新或重定向登录
- 处理网络错误:提供重试机制
常见问题与解决方案
1. 问题:JWT 被窃取怎么办?
解决方案:
- 使用短期过期时间
- 实现令牌撤销机制
- 使用设备指纹或 IP 绑定
- 启用浏览器的 SameSite Cookie 属性
2. 问题:如何处理跨域环境下的 JWT?
解决方案:
- 使用 CORS 配置允许跨域请求
- 在 Cookie 中设置
SameSite=None和Secure标志 - 考虑使用代理服务器
3. 问题:刷新令牌被窃取怎么办?
解决方案:
- 将刷新令牌存储在 HttpOnly Cookie 中
- 实现刷新令牌轮换:每次使用刷新令牌时生成新的刷新令牌
- 限制刷新令牌的使用次数和时间
4. 问题:如何处理多个设备登录?
解决方案:
- 为每个设备生成唯一的刷新令牌
- 实现设备管理功能:允许用户查看和撤销登录设备
- 考虑使用单点登录(SSO)
高级学习资源
1. 官方文档
2. 库和工具
- jsonwebtoken:Node.js JWT 库
- jose:现代 JWT 库,支持 Web Crypto API
- vueuse:提供了
useJwt组合式函数
3. 教程和文章
- JWT Authentication in Vue 3:Auth0 官方教程
- Secure Your Vue.js App with JWT:SitePoint 教程
实践练习
练习 1:实现 JWT 认证流程
- 创建一个 Vue 3 项目
- 实现登录页面和仪表板页面
- 创建
useJWT组合式函数 - 实现路由守卫保护仪表板路由
- 测试完整的认证流程
练习 2:实现 JWT 自动刷新
- 修改
useJWT组合式函数,添加令牌刷新功能 - 在路由守卫中实现自动刷新逻辑
- 测试令牌过期后的自动刷新
- 处理刷新失败的情况
练习 3:增强 JWT 安全性
- 将 JWT 存储从 localStorage 迁移到 HttpOnly Cookie
- 实现刷新令牌轮换机制
- 添加设备指纹验证
- 实现令牌撤销功能
练习 4:实现多设备登录管理
- 创建设备管理页面
- 为每个设备生成唯一的刷新令牌
- 实现设备查看和撤销功能
- 测试多设备登录场景
总结
JWT 是 Vue 3 应用中常用的认证机制,特别是在前后端分离架构中。通过深度集成 JWT,我们可以实现安全、可靠的用户认证和授权系统。在实际应用中,我们需要注意 JWT 的存储安全、刷新机制和验证逻辑,以确保系统的安全性和可靠性。
本教程介绍了 Vue 3 与 JWT 的深度集成,包括 JWT 的基本结构、认证流程、状态管理、路由守卫和最佳实践等内容。通过实践练习,你可以进一步掌握 JWT 在 Vue 3 应用中的高级用法,为构建安全可靠的 Web 应用打下坚实的基础。