你选择胖 JWT 还是瘦 JWT?

1 阅读10分钟

你选择胖 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 的架构优势在当时被无限放大:

  1. 服务间信任零成本:Service A 调用 Service B,直接透传 JWT,B 无需向 User Service 求证,本地解析即可
  2. 边缘计算友好:CDN 或边缘网关可以直接基于 JWT 中的 roles 做粗粒度路由,无需回源到中心鉴权服务
  3. 离线可用:移动端在弱网环境下,本地解析 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) :使用瘦 JWTmTLS + SPIFFE ID。服务间信任基于证书,身份验证交给 Sidecar,业务代码只关心 x-user-id Header。
  • 数据层:使用能力令牌(Capability Token) ,包含具体的资源权限签名(如"允许读取 order_id=12345"),这是 JWT 与 Macaroons 思想的结合。
策略二:懒加载与缓存(Lazy Loading + Cache)

瘦 JWT 并不意味着每次请求都查库。架构师会设计多级缓存

  1. 本地 Caffeine 缓存(服务实例内):存储热点用户的权限,TTL 30 秒,保证最终一致性
  2. Redis 集群:集中式 Token 元数据存储,支持 Pub/Sub 实时撤销
  3. ** 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 只是一个容器,真正的架构智慧在于——你知道什么时候该信任客户端携带的信息,什么时候该保持怀疑并亲自验证。这种怀疑精神,才是架构师最宝贵的品质。