1. 先说清楚JWT 到底差在哪为什么我要折腾 JWE早些时候我接到一个合作方安全评审的反馈对方指着我们登录接口返回的 Token 说“你们这个 JWT 把手机号、用户角色全放在 payload 里随便找个解码网站就能读出来。”当时我很不以为然心想 JWT 本来就是这么用的服务端验签就够了谁知道合作方直接在评审单里写了个“严重风险”。后来我回去仔细一想人家说得其实没问题——JWT 默认只做签名不做加密payload 里的信息等于明文暴露在网络上。只要 Token 被中间人截获用户身份信息就裸奔了。这次经历让我把 JWT 到 JWE 的改造真正提上日程。如果你也在做 API 安全相关的设计或者正在考虑令牌方案选型这篇文章会告诉你 JWE 是什么、它和 JWT 的关键区别、怎么落地以及我在实际改造里踩过的坑。适合后端开发、安全工程师尤其是那些“接口已经上线但 Token 设计经不起推敲”的团队参考。1.1 JWT 的“裸奔”问题我先用最直白的大白话解释一下 JWT 的构造。JWT 令牌被点号分成三段Header 头、Payload 载荷、Signature 签名标准格式长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYiLCJyb2xlIjoiYWRtaW4ifQ.aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789第一段 Header 和第二段 Payload 都只是 Base64URL 编码。很多人以为 Base64URL 就是加密这个误区特别常见。Base64 只是一种编码方式把二进制转成可见字符不包含任何密钥参与的变换。你用 Python、Java、Node 里任何语言内置的方法都能解出来比如 Node 里一句话就能看到明文const payload JSON.parse(Buffer.from(token.split(.)[1], base64url).toString()); console.log(payload); // { userId: 123456, role: admin }JWT 的签名部分确实有防篡改作用服务端可以确认 payload 没有被改过。但签名解决不了信息泄露问题——Token 落在别人手里对方虽然改不了但完全能读出你放了什么数据。这就好比你写了一张明信片虽然你签了名证明是你写的但邮递员路上随便就能看到内容。如果业务场景里 Token 中只放一个无意义的 userId泄露风险还相对可控。但现实中很多团队会把手机号、邮箱、昵称甚至用户分群标签塞进 JWT原因很简单省得每次请求都查一次数据库。结果就是积累了大规模明文用户数据的泄露面。1.2 别把 JWS 当加密用这里要理清一个很容易混淆的概念。JWT 本质上是一个统称它底下分了两套标准JWSJSON Web Signature只做签名保证完整性、防篡改。我们日常说的 JWT 大多数是 JWS。JWEJSON Web Encryption做加密保护保证机密性内容不可读。JWE 同样是一个紧凑的字符串但结构从三段变成五段用点号分隔BASE64URL(Header).BASE64URL(Encrypted Key).BASE64URL(IV).BASE64URL(Ciphertext).BASE64URL(Authentication Tag)这五段分别是加密算法参数、加密后的内容加密密钥、初始化向量、密文、认证标签。使用者拿到 JWE 后没有对应的私钥或者对称密钥只能看到一堆乱码无法还原原始内容。也就是说如果你想在令牌里直接放手机号、身份证这类敏感信息JWE 是比 JWT/JWS 更合适的选择。不过这里要提前打个预防针JWE 不是万能药它解决了机密性问题但也带来了密钥管理、令牌体积变大等新问题后面第 3 节我会讲具体方案。1.3 适合引入 JWE 的业务场景不是所有接口都需要 JWE过度加密反而拖累性能和复杂度。根据我的实际经验这几类场景最值得引入 JWE令牌内包含 PII个人敏感信息手机号、邮箱、证件号、详细地址这类数据一旦泄露就是安全事故必须加密。前端证书/票据需要携带业务数据比如某些一次性授权凭证、扫码登录的临时票据内容包含用户标识和过期时间。防嗅探的移动端 APIApp 端传输链路即使有 TLS一旦用户装了不可信证书或用代理抓包TLS 也可能被绕过。JWE 加一层应用层加密等于给数据上了双保险。第三方开放平台 Token给合作方发放的令牌里面可能要带权限范围、资源路径等信息不适合让合作方的普通开发人员直接解码看到。反过来如果 Token 里只放一个随机 ID所有业务数据依赖服务端 Redis 回源那 JWT/JWS 完全够用没必要引入 JWE 增加复杂度。方案没有绝对的好坏只有适不适合你的业务。2. JWE 核心机制拆解别急着写代码先把五个部分搞懂我第一次看 JWE 规范RFC 7516的时候最大感受是术语太多看一会儿就绕晕了。后来我用“快递柜”的类比帮自己理清了整个流程也分享给你。假设你要给服务端寄一个加密包裹你把要保护的数据Payload比如用户手机号放进一个带锁的箱子。箱子的锁用一把“临时钥匙”锁上这把临时钥匙就是CEKContent Encryption Key内容加密密钥。但你不可能直接把 CEK 给服务端于是你用服务端的公钥把 CEK 再“包”一层被包起来的 CEK 就是JWE Encrypted Key。箱子连同被包裹的临时钥匙一起寄出服务端收到后先用私钥解开 CEK再用 CEK 打开箱子拿到原始数据。整个过程设计得很巧妙同一份数据可以加密给多个接收方用不同的公钥包 CEK而不需要把数据本身重复加密多次。2.1 一次 JWE 加密请求到底发生了几件事标准的一次 JWE 加密操作内部按部就班要完成以下步骤确定加密算法组合alg密钥管理算法和enc内容加密算法。生成一个随机的 CEK长度由enc决定。比如A128GCM需要 128 位A256GCM需要 256 位。生成一个随机的 IV初始化向量GCM 模式下通常是 96 位。用 CEK 和 IV 对明文 Payload 执行内容加密得到 Ciphertext 和 Authentication Tag。用alg指定的方式把 CEK“包装”给接收方。如果用 RSA 公钥加密就叫RSA-OAEP-256如果用对称密钥包装就叫A256KW如果用直接共享密钥就叫direct。把所有元素按 Base64URL 编码拼成五段式字符串。对应到接收方验证过程完全逆向先解析 Header 看alg和enc用私钥或共享密钥解出 CEK再用 CEK 解密密文、校验认证标签。认证标签校验失败说明数据被篡改或密钥不对直接拒绝。这里面最容易忽视的是第 5 步的“密钥管理算法”和传输层加密的区别。enc只负责保护实际内容而alg决定了 CEK 怎么安全地到达对方手里。两者分工明确选型时要分别考虑强度。2.2 alg 和 enc两个参数决定安全上限在 JWE 的 Header 里alg和enc是最关键的两个参数。我建议初次使用时直接选择业界主流默认值参数作用推荐取值说明alg密钥管理算法决定 CEK 如何传递RSA-OAEP-256或ECDH-ES非对称场景首选避免在系统中硬编码共享密钥enc内容加密算法决定 Payload 的加密强度A256GCM认证加密安全性和性能平衡最好也可以使用ECDH-ES它基于椭圆曲线密钥协商生成的令牌更短适合对体积敏感的场景。但它实现细节更复杂需要妥善处理曲线参数和密钥派生函数。如果团队刚上手我建议先用 RSA-OAEP-256 跑通全流程。另外注意A256GCM这种选法表示密钥长度 256 位、GCM 模式。如果 256 位在当前合规要求下不够部分金融场景要求更高可以换到A512GCM之类的扩展组合但这类非标准组合要谨慎使用不是所有库都支持。2.3 密钥管理思路决定方案能不能长期跑JWE 落地最大的门槛不是加密算法本身而是密钥管理。我见过不少团队算法用得花里胡哨但私钥直接放在 Git 仓库里或者用同一个密钥跑了两三年一旦泄露就是全军覆没。这里我分享一套实用思路公钥私钥分离管理服务端只持有私钥用于解密 JWE客户端只持有公钥用于加密数据。公钥可以下发到前端、第三方但私钥必须放在服务端密钥管理系统或环境变量里。使用 kidKey ID标识JWE Header 中加kid字段表示“我用的是哪把密钥”。这样密钥轮换时老令牌还能用老私钥解开新令牌用新私钥签发两边互不干扰。制定轮换周期建议 90 天到 180 天轮换一次私钥。可以在系统中维护两个密钥一个当前生效的一个前一个版本的轮换期间两个都能解密。密钥到期前预热提前一周发布新公钥给调用方设置好过渡期避免切换瞬间大量请求解密失败。如果你们公司有 KMS密钥管理服务直接把私钥托管进去是最稳妥的。如果团队体量小先用环境变量加 Vault 这类工具也能接受但千万别把私钥写死在代码里。3. 实战用 Node.js 把登录令牌从 JWT 升级成 JWE理论说了这么多下面直接进入实战。我用 Node.js 生态里的jose库来演示它是目前对 JWE/JWS 支持比较完善、API 也简洁的库。如果你用的是 Java 或 Python思路完全一致底层标准是通用的只是换成对应的库而已。3.1 选型为什么用 jose 作为基础库Node.js 里实现 JWE 的库有几个选择我自己实测下来最推荐joseGitHub 上很活跃npm 包名jose。原因有三同时支持浏览器和 Node.js不需要区分环境。对 JOSE 全系标准支持完整JWS、JWE、JWK 都有对应 API。纯 JS 实现无原生模块编译问题安装直接用。Java 生态常用nimbus-jose-jwtPython 生态常用jwcrypto都是成熟方案。核心概念都基于 RFC 7516你只要理解了密钥管理和内容加密的关系切换语言只是换 API 调用而已。安装依赖npm install jose3.2 生成密钥对需要注意的细节先用 Node 内置的crypto模块生成 RSA 密钥对。注意密钥长度至少 2048 位我建议 3072 位RSA-OAEP-256 条件下安全余量更足。const crypto require(crypto); const { publicKey, privateKey } crypto.generateKeyPairSync(rsa, { modulusLength: 3072, publicKeyEncoding: { type: spki, format: pem }, privateKeyEncoding: { type: pkcs8, format: pem }, }); console.log(公钥下发给客户端:\n, publicKey); console.log(私钥服务端保存:\n, privateKey);生成后公钥可以放进一个公开配置接口客户端每次启动时拉取私钥放到服务端环境变量或 KMS 中不要进 Git。为了方便服务端区分当前使用哪把密钥我给每对密钥定义一个 Key ID并在 JWE Header 里带上kid。比如const kid rsa-2025-01;3.3 签发令牌完整代码与字段设计登录成功后服务端要生成一个 JWE 令牌返给客户端。我在这个令牌里要放的信息是用户 ID、角色和登录会话版本平时可能还会放手机号现在都被加密了不会泄露。const { SignJWT, EncryptJWT } require(jose); // 私钥要转成 JWK 格式 const privateKeyObj await crypto.subtle.importKey( pkcs8, pemToBuffer(privateKey), { name: RSA-OAEP, hash: SHA-256 }, false, [decrypt] ); // 这里我用 EncryptJWT它简化了“JWT Claims 放进 JWE”的过程 const token await new EncryptJWT({ userId: u_10086, role: admin }) .setProtectedHeader({ alg: RSA-OAEP-256, enc: A256GCM, kid }) .setIssuedAt() .setIssuer(my-api-server) .setAudience(my-api-client) .setExpirationTime(2h) .encrypt(privateKeyObj); console.log(token);上面的EncryptJWT是jose提供的高级封装它会把 JWT Claims 当作明文 Payload自动完成 JWE 加密。这样我们既保留了 JWT 原有的结构化 Claims过期时间、签发者、受众等又实现了内容加密。加密后的 Token 大致长这样五个点号分隔的段eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwi...不用看内容没有私钥就无法反推出原始用户信息。3.4 校验令牌解密、验签、再验业务字段客户端拿到 Token 后每次请求把它放在Authorization: Bearer token头里。服务端收到后走解密流程const { decryptJWT } require(jose); async function verifyJWE(token) { const privateKeyObj await crypto.subtle.importKey( pkcs8, pemToBuffer(privateKey), { name: RSA-OAEP, hash: SHA-256 }, false, [decrypt] ); // 解密并校验 Claims const { payload, protectedHeader } await decryptJWT(token, privateKeyObj, { issuer: my-api-server, audience: my-api-client, }); // 业务层面检查用户 ID、角色是否有变更 const userId payload.userId; const role payload.role; // 这里可以再查一下 Redis确认会话版本号是否还有效 return { userId, role }; }decryptJWT会帮我们完成解密 认证标签校验 Claims 校验过期时间、签发者、受众一步到位。如果 Token 被篡改、密钥错误、过期了这一层就会直接抛异常。到这里为止我们已经把令牌从“可解码明文”升级成“真正加密密文”。即使客户端到服务端之间的链路被中间人截获对方拿到的只是一串无法还原的密文。4. 踩坑实录JWE 使用时最容易翻车的几个问题JWE 本身标准成熟但在实际工程中还是有很多容易忽略的细节我把踩过的坑整理出来给大家省点时间。4.1 明文 payload 里塞了敏感信息JWE 也救不了你这句话听起来像废话但很多人真的会犯。我见过有人在 JWE 的 Header 里放用户手机号还有人把加密后的内容重新打印到日志里。JWE 只保护加密载荷Header 是明文可见的不要把任何敏感信息放进去。同时打完日志要检查是否包含完整令牌尤其是报错堆栈里容易把整个 JWE 串打出来多级日志系统会把它同步到 ES、ClickHouse反而扩大了泄露面。我的习惯是日志里只记录kid和 Token 的 MD5 前缀绝不打完整令牌原始串。需要排查问题的时候再从请求上下文里单独取。4.2 多次解密失败先看 alg 和 enc 匹配关系有一次我改密钥长度的时候出现了一批客户端解密失败查了半天发现是生成私钥的长度和alg预设不匹配。RSA-OAEP-256要求 RSA 密钥至少能承载 256 位的密钥派生输出密钥长度太短直接报错。同样ECDH-ES对曲线参数敏感A256GCM要求 CEK 是 256 位。如果和你用的库默认值不一致解密就会失败。排查思路很简单先打印解析出的protectedHeader看alg和enc是否和你生成 Token 时一致再确认私钥格式是否是pkcs8、长度是否满足要求。另外注意有些旧库对RSA-OAEP-256支持不完善建议升级到最新版本或者统一用RSA-OAEP加依赖库的默认哈希。4.3 密钥轮换怎么做不影响在线用户JWE 令牌签发后是自包含的不像 Session 要把数据存在服务端。这既是优点也是坑私钥一旦轮换老 Token 就无法解密所有在线用户会被强制下线。我采用的方案是“双密钥并行过渡”系统里维护两个私钥current和previous。所有新令牌用current私钥签发Header 里带kid。解密时先按kid选对应的私钥若失败再用previous尝试一次。一个轮换周期后把previous移除把current降级为previous再用新密钥做current。代码结构大致如下const keysByKid { rsa-2025-01: { privateKey: previousKey, publicKey: previousPub }, rsa-2025-02: { privateKey: currentKey, publicKey: currentPub }, }; function getPrivateKey(kid) { return keysByKid[kid]?.privateKey || currentKey; }这样轮换期的老令牌依然能解密新令牌使用新密钥用户无感知。4.4 性能与体积JWE 不是免费的午餐JWE 比普通 JWT 多了 RSA 加密/解密操作同时令牌体积也会明显变大。我做过一个压测在相同 Claims 条件下JWE 令牌体积比 JWS 大约大 150 到 300 字节RSA 解密开销大概增加 0.5ms 到 2ms取决于密钥长度和机器配置。如果你的服务端 QPS 很高比如单机 5 万以上RSA 解密会成为 CPU 瓶颈。这时候可以考虑以下几种优化路径换用ECDH-ES用椭圆曲线协商密钥解密开销小很多。引入缓存把解密后的结果缓存在本地内存或 Redis 中以 Token 哈希作为 key短时间命中直接返回。更极致一点用direct对称加密服务端只持有 AES 密钥解密速度快得多。但对称密钥的分发管理难度更高只适合服务间内部调用。体积变大也会影响请求头大小有些网关对 Header 长度有限制注意调整为合理阈值我遇到过 Nginx 默认large_client_header_buffers设置偏小、导致 JWE Token 放不下的情况。5. 经验总结与最终建议5.1 什么时候用 JWT什么时候用 JWE代码层面写完了最后聊一下方案边界。JWE 确实比 JWT 安全等级更高但并不意味着所有场景都该上 JWE。我的判断标准很简单令牌里需要放敏感业务数据 → 用 JWE。令牌只是“一把钥匙”所有数据靠服务端回源 → JWT/JWS 加短过期时间就够了。对接第三方开放平台合作方只需要验证签名、不需要读取数据 → JWS 合适让他们用公钥验签即可。自己内部微服务之间传递一次性票据票据里带权限上下文 → JWE 更稳妥避免内部链路被嗅探后直接泄露上下文。还有一点JWE 和 JWS 不是互斥的可以组合。比如JWE外层加密整体内部再嵌套一层 JWS防止中间环节对 Claims 做任何改动。我们现在的线上方案就是外层的 JWE 加密 内层部分字段签名兼顾了机密性和完整性。5.2 最后分享一个小技巧我在做令牌方案的时候习惯用一个很小的脚本快速验证一个 JWE 能否被正确解密避免频繁起服务看日志。这里是一个基于 Node 的独立验证片段const { decryptJWT } require(jose); const fs require(fs); (async () { const token process.argv[2]; const privateKeyPem fs.readFileSync(./private.pem, utf8); const privateKeyObj await crypto.subtle.importKey( pkcs8, pemToBuffer(privateKeyPem), { name: RSA-OAEP, hash: SHA-256 }, false, [decrypt] ); try { const { payload } await decryptJWT(token, privateKeyObj); console.log(解密成功:, payload); } catch (e) { console.error(解密失败:, e.message); } })();脚本很简单但排障效率提升很明显。遇到线上解密问题把 Token 和私钥放到临时测试环境里一跑就知道是算法配置问题、密钥不一致还是 Claims 校验失败。根据我自己的实践JWE 改造的收益是实打实的至少再也不会有人在代码评审的时候指着你的 Token 说“这里泄露了用户数据”。但它不是终点密钥管理流程、日志脱敏、监控告警这些配套工作同样重要。如果你正准备做令牌安全升级希望这篇文章能帮你少走一些弯路。
企业数字化 ERP 产品动态
相关推荐
VPU驱动开发核心:寄存器读写与内存管理实战解析 这几年做嵌入式Linux平台,VPU几乎快成了中高端SoC的标配。尤其像RK3588这类芯片,内置的VPU直接帮你把H.265、H.264的硬编硬解拉到8K级别,省下的CPU资源不是一点半点。很多搞驱动开发的朋友一听到VPU就头疼,觉得这玩意比GPU还难啃。… · 2026/9/24 19:50:03
UNION与UNION ALL的区别:去重原理、性能对比与优化实践 说实话,UNION 和 UNION ALL 这两个关键字,几乎每个写 SQL 的人都认识,但它们之间的差别绝不只是“去重”和“不去重”这么简单。我在日常做报表汇总、跨表对账、数据清洗的时候,这两个操作符几乎是天天见,但真正能把它… · 2026/9/24 19:50:03
SQL合并查询优化:UNION与UNION ALL的底层原理与性能差异 写SQL的人,大概率都背过一句口诀:UNION 会去重,UNION ALL 不去重。但真到了线上环境,面对一个跑了十几秒的合并查询,你光会背口诀是不够的。UNION 和 UNION ALL 的区别,本质上是一套完整的执行逻辑、性能模… · 2026/9/24 19:50:03
2026年组件安全扫描选型指南:商业、开源与信创方案对比 1. 组件安全扫描到底在扫什么,为什么2026年突然成了刚需组件安全扫描,圈子里更习惯叫SCA(Software Composition Analysis),说白了就是把你项目里用到的所有第三方依赖——不管是Maven拉下来的jar包、npm装的node_modul… · 2026/9/24 20:25:57
拯救者玩游戏花屏闪退,不一定是显卡驱动问题 不少拯救者游戏本用户碰到这样的故障:桌面浏览网页、看视频一切正常,只要打开大型游戏,画面就出现色块、条纹、马赛克花屏,紧接着游戏闪退,严重时直接蓝屏。很多人第一反应就是显卡驱动出问题,反复卸载、重… · 2026/9/24 20:25:51
求职焦虑自救指南:用能力定位和项目思维破局就业困境 1. 焦虑人人都有,但别被"数字"牵着走我最近后台收到不少年轻朋友的留言,都在问同一个问题:大环境不好,是不是毕业就等于失业?是不是再怎么努力也没用?说实话,只要打开社交平台&#x… · 2026/9/24 20:25:51
全开源超级签名系统部署指南:iOS内部分发与UDID签名原理详解 简介:面向需要搭建iOS应用分发与签名服务的开发者和企业,这是一套全开源的APP分发系统及超级签名系统源码,基于PHP开发,具备后台管理功能,并附详细部署文档。系统方案涵盖后台账号配置、阿里云OSS存储、七牛云下载包托… · 2026/9/24 20:25:51
Edge无法发送验证码?揭秘浏览器UA检测与兼容性问题 “全国新书目-书籍-教材查询-最全面-用chrome 浏览器才能发送验证码——用edge浏览器登入提示无法发送验证码,为何?”这个标题里的问题,我太熟了。遇到这个问题的绝对不止你一个人,它背后牵扯出的其实是很多老网站做浏览器适配时留… · 2026/9/24 20:25:39
订单多了,利润却薄了?模具注塑厂的效率困局 订单量上涨,账上利润却没同步变厚,这是当下不少模具注塑厂的真实体感。旺季产线排满,淡季又空转,摊薄下来单件成本反而走高。问题往往不在订单本身,而在从开模到量产之间的衔接损耗。有行业统计显示,制造环… · 2026/9/24 20:25:39
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44