你选择胖 JWT 还是瘦 JWT?
引言:从"去中心化"的理想说起
那年的那个深夜,当我第一次在微服务架构中写下 Authorization: Bearer <jwt> 时,我以为自己握住的是服务间通信的圣杯——无状态、自包含、横向扩展如丝般顺滑。那时的我们,正急于逃离 Session 共享的地狱(Redis 单点、Sticky Session、序列化噩梦),JWT 的出现像极了普罗米修斯的火种。
但十年后,当我凝视着那个膨胀到 8KB 的 Token 在 Header 中横冲直撞,或是在调试一个因权限延迟 200ms 而被用户投诉的接口时,我才明白:架构没有银弹,只有权衡的艺术。
让我们穿越技术史的迷雾,看看胖瘦之争如何在不同的时代语境下反复横跳。
第一章:史前时代(2010 年前)—— Session 的暴政与 JWT 的诞生
场景:单体巨石应用
在那个 SSH/SSM 框架横行的年代,HttpSession + Cookie 是身份认证的唯一真理。用户登录后,Tomcat 在内存中创建一个 Session 对象,浏览器种下 JSESSIONID。
架构痛点:
- 水平扩展之殇:Nginx 反向代理后,Session 必须共享(Tomcat 集群广播、或外挂 Redis)
- 跨域之痛:CORS 政策严格,Cookie 的 SameSite 属性让人抓狂
- 移动端适配:原生 App 处理 Cookie 总显得格格不入
JWT 的原始设计哲学
JWT(JSON Web Token)最初的设计目标很简单:让客户端携带自己的身份凭证,服务端无需保存状态。
这时的 JWT 天然是胖的——因为在单体应用里,Session 本就是"胖的",里面塞着 User 对象、Permission 列表、Department 层级。既然要替代 Session,那 JWT 必须自包含这些信息,否则每个接口都要查库,性能 regress(回退)不可接受。
典型胖 Token 结构:
{
"sub": "user_10086",
"name": "张三",
"roles": ["ADMIN", "EDITOR"],
"permissions": ["user:read", "user:write", "order:delete"],
"department": {"id": 5, "name": "架构部", "cost_center": "CC-001"},
"avatar": "https://cdn.example.com/large-image.jpg",
"iat": 1516239022
}
架构师当时的判断:既然查一次数据库生成 Token,就能避免后续 100 次接口调用都查库,何乐而不为?空间换时间是正统的优化思路。
第二章:黄金时代(2015-2018)—— 微服务蛮荒期的胖 JWT 狂欢
场景:服务大爆炸与网关的崛起
当 Spring Cloud Netflix 套件风靡一时,我们开始把单体应用肢解成数十个微服务。每个服务都需要鉴权,如果每个服务都去查数据库验证 Token,那是架构师的失职。
胖 JWT 的架构优势在当时被无限放大:
- 服务间信任零成本:Service A 调用 Service B,直接透传 JWT,B 无需向 User Service 求证,本地解析即可
- 边缘计算友好:CDN 或边缘网关可以直接基于 JWT 中的
roles做粗粒度路由,无需回源到中心鉴权服务 - 离线可用:移动端在弱网环境下,本地解析 Token 即可展示用户菜单,提升用户体验
当时的架构范式:
Client -> Gateway (验签+解析胖JWT) -> Service-A (直接用claims)
-> Service-B (直接用claims)
致命诱惑:我们开始在 JWT 中塞入越来越多的"衍生数据"——用户是否 VIP、剩余积分、甚至最近一次登录的设备指纹。毕竟, "无状态"的神话让我们误以为 Token 是免费的存储空间。
第一次危机:Token 膨胀与 Header 溢出
2017 年的生产事故让我记忆犹新:一个运营后台用户因为拥有 200+ 个细粒度权限,生成的 JWT 达到了 12KB。Nginx 默认的 large_client_header_buffers 配置无法容纳,返回 494 Request Header Too Large。更隐蔽的是,某些移动网络代理会静默截断超过 8KB 的 Header。
技术债开始显现:
- 带宽浪费:每个请求都携带 5KB 的重复信息,对于高频 API(如心跳、埋点),这是巨大的流量开销
- 敏感信息泄露风险:Base64 只是编码不是加密,胖 Token 泄露意味着攻击者获得完整的用户画像
- 撤销困境:管理员封禁用户或回收权限,Token 在过期前(通常设置 2 小时)依然有效,只能依赖"黑名单"机制,这又违背了无状态的初衷
第三章:觉醒年代(2019-2021)—— 瘦 JWT 的理性回归
场景:复杂权限与实时性的要求
随着 GDPR(通用数据保护条例)和用户隐私意识的觉醒,以及 RBAC 向 ABAC(基于属性的访问控制)演进,权限不再是静态列表,而是动态计算的:
- "工作时间 9:00-18:00 允许访问"
- "仅在 IP 段 10.0.0.0/8 内允许操作财务数据"
- "需要二次验证才能访问"
这些动态规则根本无法塞进 Token。
同时,服务网格(Service Mesh) 的兴起改变了服务间通信的方式。Istio/Envoy 接管了 mTLS 和身份传递,Sidecar 可以帮业务服务做"提权查询"(Token Introspection),胖 JWT 的自包含优势被基础设施稀释。
瘦 JWT 的架构主张
{
"sub": "user_10086",
"iss": "auth-service",
"iat": 1516239022,
"exp": 1516242622,
"jti": "unique-token-id-for-revocation"
}
仅此而已。sub(Subject)是唯一标识,具体信息通过 jti 到 Redis 或数据库查询,或调用专门的 OPA(Open Policy Agent) 策略服务。
架构师的新权衡:
| 维度 | 胖 JWT | 瘦 JWT |
|---|---|---|
| 服务依赖 | 低(自包含) | 高(依赖 Auth Service/Redis) |
| 实时性 | 差(Token 过期前无法撤销权限) | 强(每次查询最新权限) |
| 带宽 | 高(5-10KB) | 低(200-500B) |
| 延迟 | 低(本地解析) | 中(+1-5ms 网络 IO) |
| 安全撤销 | 复杂(需黑名单) | 简单(删 Redis 键即可) |
| 审计追踪 | 困难(Who carried what?) | 简单(集中式鉴权点记录日志) |
关键洞察:在微服务成熟期,网络 IO 的成本已经低于数据不一致的风险。Envoy Sidecar 与 User Service 之间 1ms 的 gRPC 调用,比忍受一个携带着过时权限的胖 Token 在系统中横冲直撞要安全得多。
第四章:现代性困境(2022-至今)—— 混合策略与上下文感知
场景:Serverless、边缘计算与多租户
今天的架构更加复杂:AWS Lambda 冷启动要求极致的低延迟,边缘函数(Cloudflare Workers/Vercel Edge) 无法直接访问中心 Redis,多租户 SaaS 需要兼顾不同客户的合规要求。
没有绝对正确的选择,只有分层治理:
策略一:分层 Token(Token Federation)
借鉴 OAuth2.0 的 Token Exchange 思想,我们在不同边界使用不同"体型"的 Token:
- 边缘层(Edge) :使用胖 JWT(包含静态角色),由 CDN 或边缘网关做快速路由决策。这里的"胖"是经过裁剪的必要信息,如
tier: "enterprise"用于区分灰度流量。 - 服务层(Service Mesh) :使用瘦 JWT 或 mTLS + SPIFFE ID。服务间信任基于证书,身份验证交给 Sidecar,业务代码只关心
x-user-idHeader。 - 数据层:使用能力令牌(Capability Token) ,包含具体的资源权限签名(如"允许读取 order_id=12345"),这是 JWT 与 Macaroons 思想的结合。
策略二:懒加载与缓存(Lazy Loading + Cache)
瘦 JWT 并不意味着每次请求都查库。架构师会设计多级缓存:
- 本地 Caffeine 缓存(服务实例内):存储热点用户的权限,TTL 30 秒,保证最终一致性
- Redis 集群:集中式 Token 元数据存储,支持 Pub/Sub 实时撤销
- ** sticky session hint**:在 Gateway 层根据
user_id哈希,尽量将同一用户路由到同一实例,提高本地缓存命中率
代码示例(伪代码):
public Mono<Permission> getPermission(String userId) {
return caffeineCache.get(userId,
k -> redisClient.get("perm:" + userId)
.switchIfEmpty(databaseQuery(userId))
);
}
策略三:隐私计算与盲签名
在 GDPR 和《个人信息保护法》背景下,Token 越胖,合规风险越大。现代架构倾向于最小必要原则:
- 不在 Token 中暴露明文邮箱,使用假名化
sub: anon_9f86d08 - 敏感权限通过策略决策点(PDP) 实时计算,而非静态携带
- 使用私有 claim(Encrypted JWT 或 Nested JWT),只有特定服务能解密,避免中间服务窥探
第五章:架构师决策框架(决策树)
经过十年的折腾,我总结了一个决策框架供你参考:
1. 先问自己三个元问题
Q1: 你的系统需要"离线可用"吗?
- 是(如 IoT 设备断网操作、纯前端加密)→ 胖 JWT(但必须短过期时间 + 硬件安全模块)
- 否 → 继续看 Q2
Q2: 权限变更需要多久生效?
- < 5 秒(金融级安全、员工离职立即失效)→ 瘦 JWT + 实时查询
- 5 分钟可接受 → 胖 JWT + 短 TTL(5 分钟)+ 黑名单
- 不需要撤销(只读系统、公开 API)→ 胖 JWT(最大化性能)
Q3: 你的基础设施拓扑是什么?
- 跨 Region/跨云,网络不稳定 → 胖 JWT(避免跨区查询 Auth 服务)
- 同机房/Service Mesh,低延迟内网 → 瘦 JWT
- Serverless/边缘节点 → 混合(边缘胖 JWT,回源后换瘦 JWT)
2. 折中方案:受控的"微胖"JWT
如果业务场景复杂,我推荐微胖策略(Fit JWT) :
{
"sub": "user_10086",
"roles": ["ADMIN"], // 只保留粗粒度角色(用于路由)
"permissions_hash": "md5:abc123", // 权限指纹,用于快速比对是否过期
"ver": 3, // 权限版本号,服务端可据此决定查不查库
"iat": 1516239022,
"exp": 1516242622
}
逻辑:
- Gateway 用
roles做粗粒度放行(如 ADMIN 直接允许访问管理后台) - Service 用
permissions_hash比对本地缓存,若 hash 匹配则跳过查库 - 若
ver不匹配,说明用户权限已变更,触发一次数据库查询并更新缓存
这实现了90% 的瘦 JWT 安全性 + 80% 的胖 JWT 性能。
结语:架构是时间的艺术
回到最初的问题:胖 JWT 还是瘦 JWT?
十年前的我选择了胖,因为想摆脱数据库的枷锁;五年前的我选择了瘦,因为无法忍受数据不一致的焦虑;今天的我选择让基础设施来决定——在边缘让 Token 胖一点,在中心让它瘦一点,在数据库前让缓存聪明一点。
架构没有正确答案,只有对约束条件的深刻理解。 当你下次设计鉴权方案时,不要问"社区推荐什么",要问:
- 我的网络拓扑允许我查库吗?
- 我的业务能容忍几分钟的权限延迟吗?
- 我的 Token 经过了多少个不可信的中间节点?
- 当用户点击"注销"时,我能否承受他还能操作 2 分钟的代价?
想清楚这些,胖瘦自然分明。
记住:JWT 只是一个容器,真正的架构智慧在于——你知道什么时候该信任客户端携带的信息,什么时候该保持怀疑并亲自验证。这种怀疑精神,才是架构师最宝贵的品质。