大概一年前我接手了一个对外API网关项目所有受保护接口的身份令牌统一用的是JWT。当时团队对JWT的评价很高无状态、跨语言、有RFC标准、签名验签也不复杂。直到一次安全评审同事拿着线上真实Token贴到jwt.io页面里几秒钟就把用户手机号、邮箱、内部工号全看光了。那一刻我才意识到我们以为在用的“安全令牌”其实只是“防篡改的明文信封”而不是私密信封。从那时起我开始认真调研JWE并在后续项目里把核心API的令牌从JWT升级成了JWE。这篇内容就是那段时间的实战沉淀适合已经被JWT“惯坏”、又希望API敏感数据真正做到端到端加密的同学参考。1. 为什么JWT不够用我在线上踩到的明文Payload坑1.1 JWT的三段式结构不是加密只是编码JWT的标准格式是Header.Payload.Signature三段Header和Payload都使用BASE64URL编码。BASE64URL不是加密算法它只是一种“编码表做了替换”的Base64变种任何人拿到Token都能在几秒钟内解码出明文内容。签名的作用是完整性校验解决的是“Token有没有被篡改”的问题而不是“Token里的信息有没有暴露”的问题。很多团队会把“签名”和“加密”混为一谈这是最危险的地方。我当时也犯了这个错以为JWT做了签名就安全了实际上Payload里的用户手机号、邮箱、工号全部裸奔。签名只是保证这些字段不能被偷偷改掉但没任何机制保证这些字段不被看到。举一个直观例子eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLTAwMSIsInBob25lIjoiMTM4MDAwMDAwMDAifQ.signature中间那段eyJzdWIiOiJ1c2VyLTAwMSIsInBob25lIjoiMTM4MDAwMDAwMDAifQ用任意一个BASE64解码工具就能得到{sub:user-001,phone:13800000000}如果把这类Token放在前端浏览器、第三方回调URL、网关访问日志里等于把用户隐私直接写在了信封正面。1.2 什么时候JWT会“裸奔”并不是说JWT完全不能用而是要搞清楚哪些场景下JWT的明文Payload会形成实际风险。我归纳了三个最容易出问题的场景第一个场景前端持有的Access Token。浏览器开发者工具里能看到完整Token一旦有人通过XSS脚本或浏览器插件把Token抓走Token里的身份证号、手机号直接就泄露了。更麻烦的是很多前端项目会把Token打点上报到日志平台等于二次扩散。第二个场景第三方OpenAPI的回调参数。我们当时有一个对外开放的接口合作方需要把用户标识传回来做回调验签。为了省事直接把整个JWT放在回调URL的state参数里结果第三方网关的访问日志把完整URL记录了下来里面包含了用户手机号。这个问题的严重性不在于签名被破解而在于明文信息根本没有保密能力。第三个场景OIDC协议中的ID Token。OpenID Connect里返回的ID Token默认就是JWS格式直接携带用户身份信息。如果内部多个服务之间继续转发这个Token用户信息就会被无关服务看到扩大了数据暴露面。1.3 什么样的接口才真正需要JWEJWE不是万能药为所有接口都上加密反而会增加复杂度和性能开销。从我的经验看下面这几类接口应该优先考虑JWEToken中携带手机号、邮箱、身份证号、家庭住址等用户隐私字段令牌需要跨越不可信的第三方网络或终端Token会出现在访问日志、回调参数、浏览器存储等易被读取的位置使用OIDC协议颁发ID Token且该Token可能被下游服务继续传递作为开放平台API的凭据凭证需要防止合作方在传输过程中逆向读取内部字段如果只是纯内部服务之间调用网络边界有mTLS保护JWT的性能和简洁性优势仍然值得保留。我现在的原则是加密留给公网和隐私签名留给内部和性能。2. JWE工作原理拆解不是“JWT加了个密”这么简单2.1 JWE紧凑序列化的五个字段先从概念上理清一个容易混淆的点JWT、JWS、JWE其实是三个维度的东西不是三个竞争方案。JWS和JWE是JOSE框架下两种并行的安全格式分别负责签名和加密而JWT是一种承载Claims的令牌容器它可以以JWS形式序列化也可以以JWE形式序列化。所以更准确的说法是JWE不是JWT的替代品而是JWT家族里的加密分支。JWE紧凑序列化结构一共有五个字段用点号分隔BASE64URL(Header).BASE64URL(EncryptedKey).BASE64URL(IV).BASE64URL(Ciphertext).BASE64URL(Tag)这五个字段分别负责什么我用一个表格说明字段作用通俗理解Header声明加密算法、密钥ID、Token类型封面上的“使用说明”EncryptedKey被公钥或共享密钥保护后的内容加密密钥保险柜里藏的临时钥匙IV加密时使用的初始化向量给每条消息加的随机盐Ciphertext真正的密文锁起来的文件本体TagAEAD认证标签用于完整性校验封条上的防伪标记2.2 alg和enc各管一段密钥管理与内容加密JWE的加密过程和我一开始想的不一样它不是拿公钥直接加密整个Payload。实际流程分两层随机生成一个临时内容加密密钥CEK比如A256GCM需要256位随机密钥用alg指定的密钥管理算法保护这个CEK比如用RSA公钥加密CEK或者通过ECDH协商派生CEK用CEK加上随机生成的IV通过enc指定的内容加密算法推荐AES-GCM对明文Payload加密最后生成密文和认证标签组装成五段式Token为什么要多此一举搞一个临时CEK这个问题我当时也困惑过。核心原因有两个RSA这类非对称加密算法处理不了长明文2048位RSA公钥加密最多也就能加密约245字节而AES-GCM在硬件加速下吞吐量极高适合加密任意长度的消息。用保险柜KEK保护临时钥匙CEK再用临时钥匙打开抽屉里的文件就是经典的混合加密思想。alg字段常见取值包括RSA-OAEP-256、ECDH-ES、A128KW等enc字段常见取值包括A256GCM、A128CBC-HS256等。需要注意EncryptedKey字段在某些算法下可能为空。比如ECDH-ES模式下CEK是通过发送方临时密钥和接收方静态密钥协商派生出来的不是加密传输的所以这个字段为空字符串但五个字段之间的点号分隔符依然保留。2.3 算法组合怎么选推荐与避雷算法选型直接决定安全和性能这块不能偷懒照抄。我把自己实测过的组合整理成一张表算法组合适用场景推荐程度RSA-OAEP-256 A256GCM公钥加密、私钥解密最通用强烈推荐ECDH-ES A256GCM性能更好适合高并发推荐但注意库兼容性A128KW A256GCM双方共享对称密钥推荐密钥管理需到位RSA1_5 A128CBC-HS256老系统兼容坚决不用A128CBC-HS256 A128CBC-HS256非AEAD组合慎用容易用错RSA1_5之所以被拉黑是因为存在Bleichenbacher攻击攻击者可以通过不断修改密文并观察解密结果来逐步还原被加密的密钥这是已经有多篇论文验证过的老问题。AES-GCM属于AEAD认证加密算法加密的同时自带完整性保护不额外需要HMAC用起来安全边界更清晰。CBCHMAC的组合虽然也能达到类似效果但加密与MAC的顺序、密钥分割这些细节很容易配错我建议新手尽量避开。3. JWE实战从依赖选型到跑通加密解密3.1 开发库选型语言生态里JWE库不少但我实际用下来最稳妥的是这两个JavaNimbus JOSE JWTMaven坐标com.nimbusds:nimbus-jose-jwt文档全、社区活跃Node.jsjosenpm包名就是joseAPI设计现代支持Web Crypto底层实现选库时有一个容易被忽略的点看它是否支持你选定的alg算法。有些老库对ECDH-ES支持不完整只支持RSA系列。我建议在技术选型阶段先用小Demo把加密、解密跑通再决定是否引入生产环境不要直接照搬别人的配置。3.2 Java示例RSA-OAEP-256 A256GCM生成RSA密钥对并签发JWEimport com.nimbusds.jose.*; import com.nimbusds.jose.crypto.RSADecrypter; import com.nimbusds.jose.crypto.RSAEncrypter; import com.nimbusds.jose.jwk.*; import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; import com.nimbusds.jwt.EncryptedJWT; // 生成2048位RSA密钥注意use必须是ENCRYPTION RSAKey rsaKey new RSAKeyGenerator(2048) .keyUse(KeyUse.ENCRYPTION) .keyID(rsa-2024-main) .generate(); RSAKey publicKey rsaKey.toPublicJWK(); // 构造JWE Header JWEHeader header new JWEHeader.Builder( JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM) .keyID(rsa-2024-main) .contentType(JWT) .build(); // 明文Payload Payload payload new Payload({\sub\:\user-001\,\phone\:\13800000000\}); // 加密 EncryptedJWT jwe new EncryptedJWT(header, payload); RSAEncrypter encrypter new RSAEncrypter(publicKey); jwe.encrypt(encrypter); String jweToken jwe.serialize();解密EncryptedJWT parsed EncryptedJWT.parse(jweToken); RSADecrypter decrypter new RSADecrypter(rsaKey); parsed.decrypt(decrypter); String plaintext parsed.getPayload().toString();这里有一个我踩过的坑生成RSA密钥时一定要设置keyUse(KeyUse.ENCRYPTION)。如果不设置Nimbus默认有可能会把密钥标记成签名用途虽然不设置也能跑但部署到严格校验use字段的网关中间件时会直接报错。同理如果有人给你提供公钥也要确认公钥的use是enc而不是sig。3.3 Node.js示例JWE包裹签名JWT的Nested JWT在实际项目里我更推荐一种进阶玩法先对Claims做JWT签名再用JWE把整个签名Token加密。这样内层签名保证数据不被篡改外层加密保证数据不被读取两件事各司其职这也是RFC里提到的Nested JWT思路。const { generateKeyPair, SignJWT, CompactEncrypt, compactDecrypt } require(jose); async function main() { // 生成RSA密钥对 const { privateKey, publicKey } await generateKeyPair(RS256); // 第一步内层签名JWT const innerToken await new SignJWT({ sub: user-001, phone: 13800000000 }) .setProtectedHeader({ alg: RS256 }) .setIssuedAt() .setExpirationTime(5m) .sign(privateKey); // 第二步外层JWE加密 const jweToken await new CompactEncrypt(new TextEncoder().encode(innerToken)) .setProtectedHeader({ alg: RSA-OAEP-256, enc: A256GCM }) .encrypt(publicKey); console.log(jweToken); // 解密还原内层JWT const { plaintext } await compactDecrypt(jweToken, privateKey); const recoveredInnerToken new TextDecoder().decode(plaintext); }如果只想签发一个加密JWT、不需要内层签名可以直接用EncryptJWTconst { EncryptJWT, jwtDecrypt } require(jose); const jwe await new EncryptJWT({ sub: user-001 }) .setProtectedHeader({ alg: RSA-OAEP-256, enc: A256GCM }) .setIssuedAt() .setExpirationTime(2h) .encrypt(publicKey); const { payload } await jwtDecrypt(jwe, privateKey);Nested JWT最大的好处是解密之后拿到的仍然是一个合法JWT下游服务如果想继续验签不需要感知加密逻辑只要用自己的公钥验内层签名即可耦合度低很多。3.4 密钥管理的正确姿势kid、JWK Set与轮换JWE的密钥管理比JWT复杂一个量级因为JWT通常只有一把验签公钥而JWE可能需要多把加密公钥并支持轮换。我目前的生产配置是这样的每个密钥生成时分配唯一kid比如rsa-2024-mainJWE Header里带上kid解密方根据kid选对应私钥用JWK Set格式管理多把公钥对外暴露一个/.well-known/jwks.json端点线上至少保留两把密钥一个当前生效一个旧密钥用于宽限期解密一份标准的JWK Set公钥长这样{ keys: [ { kty: RSA, kid: rsa-2024-main, use: enc, alg: RSA-OAEP-256, n: 这里是RSA模数的BASE64URL编码, e: AQAB } ] }密钥轮换时不要把旧私钥直接删掉。正确做法是先发布新公钥让客户端逐步切换等确认所有存量Token都解密成功后再在密钥库里移除旧私钥。我见过一个团队因为轮换时直接删旧密钥导致线上还有大量旧Token的用户全部登录失败回滚也没用只能强制用户重新登录这是个惨痛教训。4. JWE与JWT的对比怎么选型才不后悔4.1 不要非此即彼三个概念先理清网上很多文章把JWE和JWT放在对立面我觉得不够准确。准确的关系是这样的JWS基于签名的JOSE格式三段式保证完整性和不可否认性JWE基于加密的JOSE格式五段式保证机密性和完整性JWT承载Claims的令牌容器既可以用JWS序列化也可以用JWE序列化所以标题里说的“比JWT更上一层楼”更严谨的理解是“用JWE这个加密格式来承载JWT Claims比单纯用JWS格式签名更安全”。它不是要替代JWT而是在JWT外面再加一道保险柜门。4.2 一张表格看清差异维度JWTJWS格式JWE格式Nested JWTJWE包裹签名JWTPayload可见性BASE64URL直接解码可见密文不可见密文不可见防篡改签名保证AEAD认证标签保证内层签名外层认证标签Token体积最小较大最大性能验签快解密较慢验签解密最慢调试便利性jwt.io直接看需要私钥解密需要私钥解密再验签典型场景内部服务间无状态鉴权公网API隐私令牌第三方开放平台/OIDC ID Token这个表格是我做技术方案时最常用的对照表。绝大部分情况下内部服务用第一列就够了公网API和涉及隐私字段的令牌用第二列或第三列。4.3 我项目里的折中方案回到我接手那个API网关项目最终落地的方案不是一刀切全换JWE而是分了两条链路对外暴露的OpenAPI接口以及所有返回给浏览器前端的令牌统一改成Nested JWT内层是RS256签名的JWT外层是RSA-OAEP-256A256GCM的JWE。内层签名保证Token在解密后依然可以被各个服务独立验签外层加密保证传输过程中和日志里看不到任何用户隐私。内部服务之间的调用为了性能考虑仍然保留普通JWT。因为内网环境有mTLS加密传输再加上网络ACL控制信息泄露风险已经很低这时候用JWE就是纯属给CPU找活干。这个方案的核心理念是安全性要跟着数据暴露面走不能所有接口一套令牌方案打天下。4.4 需要谨慎使用JWE的场景JWE也不是没有代价。如果遇到下面这几种情况我建议你先把JWE放一放想清楚再说场景一Token要放在URL里。JWE体积比JWT大很多一个普通JWT可能三四百字节JWE轻松超过1KB甚至2KB。URL长度受浏览器、网关、中间件多重限制放不下是常有的事。场景二极高并发且无状态服务。RSA-2048的解密操作是CPU密集型的比JWT验签慢一两个数量级。虽然单次毫秒级看起来不吓人但几万QPS放大下来对网关算力有明显压力。场景三没有完整的密钥管理基础设施。JWE的性能和安全很依赖密钥管理系统。如果团队连kid都不知道是什么私钥直接躺在Git仓库里那用了JWE反而更危险因为加密密钥泄露比签名密钥泄露更难被发现。5. 踩坑与优化JWE上线后的性能、缓存与排错5.1 体积膨胀是意料之中也是意料之外我第一次把线上JWT换成JWE时Token体积从大约300字节膨胀到了将近2KB直接导致三个问题Cookie存储放不下、日志存储成本上升、前端接口响应体变大。这个问题不能靠“忍一忍”解决要想办法控制。我的做法是精简Claims字段。JWT时代习惯了往里塞用户名、部门、角色、权限列表JWE时代这些能省则省。最终我的外层JWE只保留了sub和必要的时间字段其他信息全部走sub去后端查询。Token体积降到了900字节左右安全性反而更好因为敏感信息不再跟着每个请求到处跑。5.2 解密失败排查清单JWE上线后最让人头疼的就是解密失败。我整理了一份排查清单基本能覆盖90%的线上问题密钥不匹配。检查kid在钥匙串里是否存在不同环境测试/预发/生产的密钥是否搞混。这个占了我遇到问题的半数以上。算法与密钥类型冲突。用RSA私钥去解ECDH-ES加密的Token或者反过来都会直接报错。检查Header里的alg字段和仓库里的密钥类型是否一致。Token被截断或二次编码。JWE五段结构很长放到URL后容易被代理截断。另外有些语言收到Token后会自动做一次URL decode把Base64URL的-和_转成别的字符导致解析失败。服务器时钟偏差。JWE标准支持nbf和exp字段校验时会和服务器当前时间比较。如果服务器时间差得太多明明没过期的Token也会报错。用NTP校准时钟是基本操作。BASE64URL Padding问题。某些库要求BASE64URL必须去掉填充符有些老库又要求必须有填充符。如果你的加密方和解密方用了不同语言的库这一点最容易踩雷。5.3 提升校验性能的实操技巧JWE的RSA解密确实比JWT验签慢但实际优化空间也很大。我在生产环境里用了三个方案效果明显方案一只解密一次Claims透传。网关入口做一次JWE解密后把Claims转成内部请求头传给下游服务下游不再重复解密。这样整条链路里RSA解密只发生一次性能损失可控。方案二Redis缓存解密结果。对于同一个Token在短时间内被反复校验的场景比如网关限流插件和业务服务都会校验可以用Token的完整字符串作为Key把解密后的Claims JSON缓存在Redis里TTL设3到5分钟。不过要注意缓存必须同时校验exp字段不能在缓存层把过期Token放行。方案三按客户端分密钥。给不同客户端分配不同的kid和密钥可以避免所有高并发流量挤在同一把RSA私钥上同时也方便针对某一客户端单独做密钥轮换。5.4 上线前必须做的检查项最后分享几个我是在出过一次事故后才总结出来的上线检查项第一确认密钥绝对不在Git仓库里。我见过有人把JWK Set JSON直接提交到配置文件里这个等同把保险柜钥匙放在保险柜旁边。生产环境的密钥应该交给托管密钥服务拿不到密钥的人即使拿到JWE代码库也解不出明文。第二日志脱敏规则要提前设计。JWE本身已经隐藏了明文Claims但有些团队图方便把解密后的Claims直接打日志这就等于自己把保险柜门打开给人看。我现在的做法是不允许在任何业务日志里记录解密后的Claims只允许记录Token前几位用于排查定位。第三压测脚本必须覆盖加密和解密两条路径。JWT验签和JWE解密的性能特征完全不同不要拿旧压测数据来评估新方案。我当时的经验是JWE解密链路整体吞吐量比JWT下降了60%左右但通过缓存和透传方案可以明显缓解。如果让我重新做一次那个网关项目我会从第一天就画清楚“哪些链路要加密哪些链路只要签名”。JWE并不是要取代JWT它是在JWT外面加的那道保险柜门。现在我们的对外API已经全部切换为Nested JWT方式安全评审再也没拿“明文Claim”说事虽然排查问题偶尔要写解密脚本但比起用户隐私裸奔这个代价完全值得。
企业数字化 ERP 产品动态
相关推荐
Python代码质量守门员:Pylint与Flake8完整实战指南 前两天同事找我诉苦:一个功能全部写完,往仓库一推,CI直接红了。拉下来看日志,不是测试没过,是Lint卡住了——有个没用的import,还有个函数命名不够规范。这场景大家应该都不陌生。代码质量这件事࿰… · 2026/9/24 19:49:43
VC++运行库缺失全解决:从DLL报错到一键安装全家桶 1. 为什么你的电脑总在缺运行库:先从一次真实的报错说起有一次帮同事装一个工业仿真软件,双击安装包一切正常,结果软件装好一启动直接弹窗:“无法启动此程序,因为计算机中丢失 MSVCP140.dll。尝试重新安装该程序以解决… · 2026/9/24 19:49:37
番茄工作法在软件测试中的实战应用与落地指南 我想先聊一个场景:你坐在工位上,刚把一条用例的前置数据准备好,正准备开始执行,微信弹了需求变更,紧接着测试环境挂了,等环境的时候顺手刷了十分钟网页,等环境好了,刚才那条用例的逻… · 2026/9/24 20:24:23
外贸必备:集装箱类型、尺寸对照与装柜计算全攻略 做外贸这些年,我最大的体会是:很多新手一开始把精力全扑在找客户、谈价格上,结果货快出了,却在"装什么柜子、能装多少、怎么装"上栽了跟头。集装箱的类型与尺寸,看似是物流环节里最不起眼的基础知识… · 2026/9/24 20:24:23
重组人IL-6蛋白实验应用全攻略:从信号通路到临床转化 在生物医学实验室泡久了的人,对IL-6这个名字绝对不会陌生。白介素-6(Interleukin-6)可以说是整个炎症网络里最核心的枢纽分子之一,几乎所有跟免疫、炎症、肿瘤、自身免疫病相关的课题,绕来绕去都会碰到它。但真正动手去… · 2026/9/24 20:24:17
IL-6重组蛋白研究从信号通路到临床应用的完整指南 我们实验室和IL-6打交道快十年了,从最初拿重组蛋白做细胞增殖实验,到后来用各种突变体和中和抗体去拆解信号通路,再到近几年参与几个抗体药物的临床前评估,这一路踩过的坑、积累的经验,确实值得好好写一写。很多人问我… · 2026/9/24 20:24:17
Flask+微信小程序构建寻亲平台:全栈实战与部署指南 “宝贝回家”这几个字,对做技术的人来说,不应该只是新闻里的感人故事。它背后是一个极其典型的 Web 全栈实战场景:地理位置、图片存储、模糊搜索、状态流转、消息通知,全部都在一个小程序里。用 Flask 做后端,配合微信… · 2026/9/24 20:24:17
蓝牙SoC产线反复升级问题排查:以中科蓝讯BT5756C为例 做蓝牙音频方案这些年,中科蓝讯的芯片没少折腾,BT5756C算是我手里出镜率比较高的一颗。前两天刚好有个做耳机的客户找过来,说产线测试盒升级固件的时候遇到了个怪现象:固件烧进去了,板子也重启了,可没跑两秒… · 2026/9/24 20:24:17
基于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