导读:本期聚焦于小菜鸟创作的《Vue 3 中如何工程化实现 JWE 加密 JWT 令牌?完整方案详解》,敬请观看详情。普通的 JWT 只是做了 Base64 编码,任何人拿到令牌都能直接读出里面的用户信息,这在涉及手机号、权限范围等敏感数据时是明显短板。JWE(JSON Web Encryption)通过内容加密把整个令牌负载变成密文,配合 A256GCM 算法既能保证机密性又能防篡改。本文围绕 Vue 3 前端工程,介绍 jose 库的选型理由、对称密钥与非对称密钥两种方案的差异,演示如何在 axios 拦截器中生成和携带 JWE 令牌、如何处理令牌轮换与过期刷新,并给出密钥存储、Vite 环境变量配置等工程化细节,帮助你在真实项目中安全落地 JWE 方案。

JWT 默认只是对 Header 和 Payload 做 Base64Url 编码,令牌一旦被截获,里面的用户 ID、角色权限等信息就完全暴露。如果业务中需要在令牌里携带手机号、身份证号或者内部权限标识,就需要用到 JWE(JSON Web Encryption)。JWE 是 RFC 7516 定义的标准,它把整个负载真正加密成密文,没有密钥的第三方即使拿到令牌也无法解密。这篇文章讲清楚在 Vue 3 项目中如何从零到一工程化地落地 JWE 方案。

Vue 3 中如何工程化实现 JWE 加密 JWT 令牌?完整方案详解

一、先理解 JWE 和 JWT 的关系

很多人把 JWT 等同于 JWS(JSON Web Signature),其实 JWT 只是一个统称概念,具体实现有两类:JWS 负责签名防篡改,JWE 负责加密防泄露。一个标准的 JWT 令牌如果是 header.payload.signature 三段式结构,那就是 JWS;如果是五段式结构 header.encryptedKey.iv.ciphertext.tag,那就是 JWE。

JWS 的问题在于 Base64 不是加密,atob() 一行代码就能还原明文。JWE 则采用混合加密体系:用内容加密密钥(CEK)对负载做对称加密(通常选 AES-GCM),再用密钥管理算法把 CEK 加密传递。如果采用 dir 直连模式,双方共享一个对称密钥;如果采用 RSA-OAEP 或 ECDH-ES,则用非对称密钥协商,前端用公钥加密,后端用私钥解密。

需要强调一点:JWE 保护的是机密性,它本身不提供防篡改能力(除非使用 AES-GCM 这种认证加密模式)。在认证授权场景下,后端验证通过后仍然要配合 HTTPS 传输,这是底线,不能因为用了 JWE 就放松。

二、密钥方案选型与 jose 库

前端生态里做 JWE 首选 jose 库,它是纯 JavaScript 实现,支持 Web Crypto API,Vue 3 配合 Vite 引入没有任何障碍。安装很简单:

npm install jose

密钥方案有两种选择。第一种是 dir 模式,前后端共享同一个 256 位对称密钥,实现最简单,性能也最好,但密钥要打包进前端代码,本质上只是混淆而不是保密,适合内部系统或对安全等级要求不极端的场景。

// src/utils/crypto.js
import { EncryptJWT, jwtDecrypt } from 'jose'

// 从环境变量读取 base64 编码的密钥并还原
const secret = new TextEncoder().encode(
  window.atob(import.meta.env.VITE_JWE_SECRET)
)

// 加密负载,生成 JWE 令牌
export async function encryptToken(payload) {
  return await new EncryptJWT(payload)
    .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
    .setIssuedAt()
    .setIssuer('vue-app')
    .setExpirationTime('2h')
    .encrypt(secret)
}

// 解密 JWE 令牌
export async function decryptToken(token) {
  const { payload } = await jwtDecrypt(token, secret, {
    issuer: 'vue-app'
  })
  return payload
}

第二种是 RSA-OAEP-256 非对称模式。后端生成密钥对,公钥通过接口下发给前端,前端用公钥加密,只有持有私钥的后端能解密。这样即使前端代码被完全分析,攻击者也拿不到解密能力。密钥对生成一次后缓存在内存中即可,不必每次请求都拉取:

import { importSPKI, EncryptJWT } from 'jose'

let publicKeyCache = null

async function getPublicKey() {
  if (publicKeyCache) return publicKeyCache
  const pem = await fetch('/api/auth/public-key').then(r => r.text())
  publicKeyCache = await importSPKI(pem, 'RSA-OAEP-256')
  return publicKeyCache
}

export async function encryptWithRSA(payload) {
  const key = await getPublicKey()
  return await new EncryptJWT(payload)
    .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
    .setIssuedAt()
    .setExpirationTime('30m')
    .encrypt(key)
}

两种方案的取舍:内部管理后台用 dir 足够;面向公网的 C 端应用、令牌里含高敏信息时,建议用非对称模式,代价是令牌体积会大一些(五段式中 encryptedKey 部分更长),加密耗时也略高。

三、axios 拦截器集成与令牌刷新

工程化的关键是把加密逻辑收敛到统一位置,业务代码完全不感知。借助 axios 拦截器,可以在请求发出前自动给负载加密,在响应回来后自动解密刷新令牌。以下是一个可直接落地的封装:

// src/utils/request.js
import axios from 'axios'
import { encryptToken, decryptToken } from './crypto'

const service = axios.create({ baseURL: '/api', timeout: 15000 })

service.interceptors.request.use(async (config) => {
  const token = localStorage.getItem('jwe_token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  // 需要加密传输的业务参数
  if (config.__encryptPayload) {
    config.headers['X-Encrypted-Body'] = await encryptToken(config.data)
    config.data = null
  }
  return config
})

service.interceptors.response.use(async (response) => {
  // 后端返回的刷新令牌也是 JWE 格式,解密后重新存储
  const newToken = response.headers['x-refreshed-token']
  if (newToken) {
    localStorage.setItem('jwe_token', newToken)
  }
  return response
})

export default service

这里有一个容易踩的坑:令牌过期时间要和刷新机制对齐。JWE 里设置的 exp 声明后端会校验,前端可以提前在路由守卫里解密判断剩余时间,低于阈值就先调刷新接口,避免出现用户操作到一半被踢出登录的情况:

// src/router/index.js 中使用
import { decryptToken } from '@/utils/crypto'

router.beforeEach(async (to, from, next) => {
  const token = localStorage.getItem('jwe_token')
  if (!token) return next('/login')
  try {
    const payload = await decryptToken(token)
    const remain = payload.exp * 1000 - Date.now()
    if (remain < 5 * 60 * 1000) {
      // 剩余不足 5 分钟,静默刷新
      const { data } = await service.post('/auth/refresh')
      localStorage.setItem('jwe_token', data.token)
    }
    next()
  } catch (e) {
    // 解密失败说明令牌非法或被篡改
    next('/login')
  }
})

四、密钥管理与工程化细节

密钥管理是整个方案的重心。使用 dir 模式时,密钥不要明文写进源码,正确做法是放在 Vite 环境变量文件中,并做 Base64 编码处理,同时在 .gitignore 中排除 .env.local。密钥内容可以用 Node 一次性生成 32 字节随机值:

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

把输出结果写进 .env.local 的 VITE_JWE_SECRET 变量即可。要清楚认知到,任何进入前端代码的信息都不是绝对保密的,所以对称密钥方案的价值在于防止令牌内容被随意阅读,而不是对抗专业逆向。真正需要高安全等级时,切换到非对称方案才是正解。

另外几点工程细节值得注意:第一,alg 和 enc 头部参数要写死在代码里,不要由外部输入决定,防止算法混淆攻击;第二,生产环境务必配合 CSP 策略限制脚本来源;第三,前后端约定好时钟偏移容忍值,避免分布式部署时因服务器时间不一致导致令牌提前失效;第四,如果项目用了微前端或 iframe 嵌套场景,密钥派生逻辑要收敛到统一的共享包里,避免多个子应用各自维护一份导致轮换不同步。

最后总结一下整体思路:JWE 解决的是令牌内容泄露问题,在 Vue 3 中配合 jose 库可以很低成本地接入,通过 axios 拦截器和路由守卫把加密、刷新逻辑全部收敛到基础设施层,业务代码零侵入。方案本身不难,难的是密钥管理和令牌生命周期的工程化设计,把这些细节处理好,JWE 在真实项目中就能稳定发挥作用。

Vue 3JWEJWT加密修改时间:2026-09-09 22:16:53

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0909/53629.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。