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 认证流程

  1. 用户使用凭证登录
  2. 服务器验证凭证并生成 JWT
  3. 服务器将 JWT 返回给客户端
  4. 客户端存储 JWT(通常在 localStorage 或 Cookie 中)
  5. 客户端在后续请求中携带 JWT
  6. 服务器验证 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=NoneSecure 标志
  • 考虑使用代理服务器

3. 问题:刷新令牌被窃取怎么办?

解决方案

  • 将刷新令牌存储在 HttpOnly Cookie 中
  • 实现刷新令牌轮换:每次使用刷新令牌时生成新的刷新令牌
  • 限制刷新令牌的使用次数和时间

4. 问题:如何处理多个设备登录?

解决方案

  • 为每个设备生成唯一的刷新令牌
  • 实现设备管理功能:允许用户查看和撤销登录设备
  • 考虑使用单点登录(SSO)

高级学习资源

1. 官方文档

2. 库和工具

  • jsonwebtoken:Node.js JWT 库
  • jose:现代 JWT 库,支持 Web Crypto API
  • vueuse:提供了 useJwt 组合式函数

3. 教程和文章

实践练习

练习 1:实现 JWT 认证流程

  1. 创建一个 Vue 3 项目
  2. 实现登录页面和仪表板页面
  3. 创建 useJWT 组合式函数
  4. 实现路由守卫保护仪表板路由
  5. 测试完整的认证流程

练习 2:实现 JWT 自动刷新

  1. 修改 useJWT 组合式函数,添加令牌刷新功能
  2. 在路由守卫中实现自动刷新逻辑
  3. 测试令牌过期后的自动刷新
  4. 处理刷新失败的情况

练习 3:增强 JWT 安全性

  1. 将 JWT 存储从 localStorage 迁移到 HttpOnly Cookie
  2. 实现刷新令牌轮换机制
  3. 添加设备指纹验证
  4. 实现令牌撤销功能

练习 4:实现多设备登录管理

  1. 创建设备管理页面
  2. 为每个设备生成唯一的刷新令牌
  3. 实现设备查看和撤销功能
  4. 测试多设备登录场景

总结

JWT 是 Vue 3 应用中常用的认证机制,特别是在前后端分离架构中。通过深度集成 JWT,我们可以实现安全、可靠的用户认证和授权系统。在实际应用中,我们需要注意 JWT 的存储安全、刷新机制和验证逻辑,以确保系统的安全性和可靠性。

本教程介绍了 Vue 3 与 JWT 的深度集成,包括 JWT 的基本结构、认证流程、状态管理、路由守卫和最佳实践等内容。通过实践练习,你可以进一步掌握 JWT 在 Vue 3 应用中的高级用法,为构建安全可靠的 Web 应用打下坚实的基础。

« 上一篇 Vue 3 与 OAuth 2.0 高级应用 下一篇 » Vue 3 国际化和本地化进阶