☰
JWT Payload与Claims详解:从三段结构到七个标准字段的工程实践
2026/10/11 19:47:16 网站建设 项目流程

几乎所有写过后端接口的开发者,都经历过这样一个场景:登录接口返回了一长串token,你把它粘贴到jwt.io上,中间那段Base64字符串里清清楚楚写着用户ID、角色、过期时间,有时候甚至能看到手机号和邮箱。那段字符串就是JWT的Payload载荷,里面一个个键值对就是Claims。可惜大多数人对它的理解停在"键值对"这个层面——到底应该放哪些字段、服务端必须校验哪些字段、哪些字段绝对不能往里放,很多人是踩了坑之后才真正搞明白的。

这篇文章我打算把JWT的Payload和Claims一次性讲透。从JWT的三段结构开始,讲清楚注册声明、公开声明、私有声明的边界,再把七个标准Claims逐个拆开看行为细节,最后用jwt.io完整走一遍编码、解码、校验的流程,并把我实际开发中遇到过的坑一并交代。写给自己备忘,也写给刚接触JWT的同学做参考。

1. 先理解JWT的三段结构:Header、Payload、Signature各管什么

1.1 一个Token到底长什么样

JWT的全称是JSON Web Token,它不是一个随机字符串,而是三段用英文句点分隔的Base64URL编码文本。随便找一个真实的Token,大概是这样的:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

把它按句点切开,就得到三个独立部分:

  • 第一段eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9:Header,描述签名算法和类型。
  • 第二段eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ:Payload,也就是本文的主角,携带实际业务数据。
  • 第三段SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c:Signature,对前两段内容做的签名,用来防篡改。

用Base64解码第一段,你会看到类似这样的JSON:

{ "alg": "HS256", "typ": "JWT" }

其实Header里的信息很少,就两个关键字段:alg告诉我们签名用什么算法,typ声明这是一个JWT。有些实现还会加kid(Key ID),用来标识服务端用哪把密钥验签。Header的作用是给验证方提供"如何验签"的元信息,它本身不携带任何业务数据。

1.2 Payload为什么被误认为"加密内容"

这是我在答疑时碰到最多的问题。很多同学看到Payload是一串看不懂的字符,就以为它是加密的,觉得把密码、手机号放进去也很安全。这个认知是错的,而且错得很危险。

Payload用的编码叫Base64URL,它和Base64是同一族的东西,只是把+替换成了-,把/替换成了_,并去掉末尾的=填充符,以便在URL里安全传输。它本质上就是一段"可逆的编码",你不需要任何密钥,拿任何一个在线工具都能把它解码回原始JSON。

Signature做的事情也不是加密,而是防篡改。只要Header或Payload中的任何一个字符被改动,用同样的密钥重新计算签名就会得到完全不同的结果,验签方就能立刻发现Token被人动过手脚。但请注意,签名只保证"没人篡改",不保证"别人看不懂"。这两件事经常被混为一谈。

所以开篇请先记住三条铁律:

  • Header负责描述算法,Payload负责携带数据,Signature负责保证前两者不被篡改。
  • Base64URL编码不是加密,任何人都能无损解码Payload。
  • 敏感信息进入Payload之前,先问自己一句:明文发给客户端,我能接受吗?

2. Claims的三种来源:注册声明、公开声明、私有声明的边界在哪里

在JWT规范(RFC 7519)里,Payload被定义为"一组Claims的集合"。所谓Claim,就是"关于某个实体的一个事实断言",落到JSON里就是一个键值对。规范把Claims分成三类:注册声明、公开声明、私有声明。

2.1 注册声明:规范强制约定的字段

注册声明是一组由RFC 7519预先定义好的字段名,它们有标准化的语义,任何遵循规范的实现都应该理解这些字段。总共有7个:iss、sub、aud、exp、nbf、iat、jti。

这些字段不是可选的摆设。虽然规范里它们的约束级别大多是"建议使用"(RECOMMENDED)而非"必须"(REQUIRED),但在真实生产环境中,签发方和验证方只要协同工作,exp、iss、aud这几个基本就是标配。我在第3部分会逐个细讲。

2.2 公开声明:IANA注册表中的"公共词汇"

公开声明指的是在IANA的"JSON Web Token Claims"注册表中登记过的字段名,或者使用URI作为字段名以避免冲突的字段。

IANA注册表里有很多现成的字段可供复用,比如:

  • name:用户的全名
  • given_name:名
  • family_name:姓
  • preferred_username:偏好用户名
  • email:邮箱地址
  • email_verified:邮箱是否已验证
  • phone_number:手机号
  • picture:头像地址
  • locale:语言区域
  • updated_at:资料最近更新时间

为什么要用注册表里的名字?因为大家都按同一套词汇表说话,字段语义一致,不同团队、不同系统之间对接时不会产生歧义。比如业务A自定义一个字段叫name表示用户昵称,业务B也定义name表示真实姓名,两边对接时就会出幺蛾子。而用IANA注册表里的name,大家默认就是"全名"。

另一种公开声明的做法是使用可解析的URI作为键名,比如"https://example.com/claims/tenant_id": "tenant_123"。这种命名方式能最大程度避免碰撞,因为域名是全局唯一的。很多跨公司、跨平台开放API场景下会采用这种写法。

2.3 私有声明:业务方自己定义的键值对

私有声明就是签发方和验证方私下约定的字段,不注册、也不做全局标准化。比如最常见的:

{ "user_id": 10086, "role": "admin", "permission": ["read", "write"] }

这类字段是业务需求的直接产物,该怎么定义完全看系统设计。但使用私有声明要注意一个问题:键名可能和注册声明撞车。比如你自定义一个exp表示"体验分",那就和标准的"过期时间"冲突了,验证方拿到Token后会产生严重误解。规范里对这类情况的处理办法是:如果自定义含义,就不要使用标准字段名;如果确实需要类似语义,就给键名加命名空间前缀。

我个人见过不少团队在私有声明上踩的另一个坑是:字段越加越多,一个Token被塞进几十个键值对,最终导致整个Token体积飙到几KB。Claims数量应该克制,只放验证方真正需要的信息,具体我在第5部分再展开。

3. 七个标准Claims逐个拆解,以及它们之间的配合关系

注册声明的7个字段,我建议你按照"身份链、时间链、防重放"三个维度去理解。

3.1 iss、sub、aud:信任链的三块基石

iss(Issuer,签发者):表示Token是谁签发的,取值通常是一个字符串或URI,比如"https://auth.example.com"。验证方拿到Token后的第一步,往往是检查iss是不是自己信任的那台签发服务器。在微服务架构里,如果网关、订单服务、用户服务都各自校验Token,iss就是第一道身份过滤器——不是我家签发的Token,直接拒绝。

sub(Subject,主题):表示Token的主体是谁,通常放用户ID、用户唯一标识,取值是字符串。注意,sub必须在签发者范围内唯一。也就是说,同一台签发服务器签发的所有Token里,同一个sub必须对应同一个用户。使用sub时不要直接放主键自增数字,而应该放一个全局唯一且不对外暴露敏感信息的标识。

aud(Audience,受众):表示这个Token是"发给谁用的"。它的取值可以是一个字符串,也可以是一个字符串数组。比如一个Token同时要被订单服务和支付服务使用,aud可以写成["https://orders.example.com", "https://pay.example.com"]。资源服务器校验时,必须检查自己的标识是否在aud列表里,否则就拒绝。aud是防止Token被"张冠李戴"的关键手段——一个发给A服务的Token,不应该能直接拿去访问B服务。

这三者的配合逻辑通常是:iss决定"我信不信这个签发者",sub决定"这是谁",aud决定"这个Token有没有资格用在我这儿"。三者都不是可选的摆设,缺少任何一个都会留下信任链漏洞。

3.2 exp、nbf、iat:时间戳三兄弟的使用与教训

exp(Expiration Time,过期时间):最常见也最重要的字段。它的取值是NumericDate,即从Unix纪元(1970-01-01T00:00:00Z)开始经过的秒数。服务端校验时,只要当前时间大于等于exp,Token就视为过期。很多人习惯在客户端做过期判断,但安全上必须以服务端时间为准,并且要预留一定的时钟偏移容忍窗口,比如允许±30秒,避免服务器和客户端时钟不同步导致误判。

nbf(Not Before,生效时间):表示Token在何时之前不可用。它的语义和exp相反——nbf之前Token无效,nbf之后Token生效。这个字段在灰度发布场景里很好用:你可以提前发放一批Token,指定它们在某个时间点之后才允许访问新版本接口。

iat(Issued At,签发时间):记录Token是什么时候签发的。它本身不参与合法性判断,但非常有用——排查问题、估算Token生命周期、配合重放检测时都离不开它。

时间戳三兄弟最容易踩的坑是单位混淆。exp、nbf、iat的值必须是秒,不是毫秒。JavaScript里Date.now()返回的是毫秒数,直接用会得到一个比正确值大1000倍的时间戳,看起来"过期时间在千年之后",Token永远不过期,安全隐患极大。正确写法是:

// JavaScript生成JWT的Payload时 const payload = { iss: 'https://auth.example.com', sub: 'user_123456', aud: 'https://api.example.com', iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 7200 // 2小时后过期 };

另外还有一个工程细节:签发Token时服务器框架一般会自动带上iat,但exp必须由业务自己算好再放进去。我看到有的同事把exp和iat写成相同值,导致Token签发即过期,排查了半天才发现是时间戳算错了。

3.3 jti:容易被忽略的防重放标识

jti(JWT ID):Token的唯一标识,取值是字符串,用于防止Token被重复使用(重放攻击)。虽然jti在规范里是"可选"的,但在高安全场景下我强烈建议加上。

实现思路是:签发时给每个Token生成一个UUID作为jti,服务端维护一个"已使用jti"列表或"黑名单"。收到Token时先校验签名,确认有效后再查jti是否已经出现过,如果出现过就拒绝。配合短时效的Token,能有效抑制重放风险。不过jti的管理需要额外的存储和查询开销,普通内部系统可以不启用,但面向支付、提现这类操作时最好加上。

4. jwt.io实操:编码、解码、校验一条完整链路

工具层面,最常用、最直观的就是jwt.io这个在线站。它没有复杂的命令,打开页面就是你熟悉的三个输入框和一段实时拼装出来的Token。下面我按"编码、解码、校验"三个动作,把完整操作链路走一遍。

4.1 在jwt.io上手工编码一个Token

打开jwt.io首页,页面左侧从上到下依次是Header输入框、Payload输入框、Secret输入框,右侧是实时生成的Token和自动解码后的三段内容。

第一步,确认Header。默认值通常是{"alg":"HS256","typ":"JWT"},一般不用改。如果你想用非对称算法RS256,把alg字段改成RS256,页面会自动弹出公钥/私钥输入框。

第二步,填写Payload。我这里用一个包含标准Claims和私有Claim的完整示例:

{ "iss": "https://auth.example.com", "sub": "user_123456", "aud": "https://api.example.com", "exp": 1770000000, "iat": 1769992800, "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "role": "admin" }

第三步,填写Secret。使用HS256时,Secret就是双方共享的对称密钥。注意HS256的密钥长度必须至少256位,也就是32字节。你要是填了123456这种短密钥,很多库运行时会直接报错,即便不报错,短密钥也很容易被暴力破解。任选一个32字符以上的随机字符串填入即可。

第四步,观察右侧。你会发现Token的三段内容随着你的输入实时变化:Header和Payload各自被Base64URL编码,第三段签名是这两段文本配合Secret计算出来的结果。你可以试着改一下Payload里的任意字符,右侧第三段签名会立刻变成完全不同的字符串——这就是"防篡改"的直观体现。

提示:HS256是对称签名,签发的密钥和验证的密钥是同一个,Secret一旦泄露,任何人都能伪造合法Token。生产环境绝对不要把Secret提交到代码仓库里,应该放在环境变量或密钥管理服务中。

4.2 解码已有Token,读懂它的Payload

手头有现成的Token时,直接把完整Token粘贴到jwt.io右侧的"Encoded"输入框,左侧会自动解码出Header、Payload和Signature三部分。解码动作本身不依赖任何密钥,谁都能做,这也是为什么反复强调"别把敏感信息放Payload"。

解码之后,页面会用JSON格式清晰展示每一个Claims。这时候你应该养成的习惯是:拿到一个Token,先看它的iss是不是自己信任的签发源,再看aud里有没有自己,再看exp是否已过期,最后才去读取sub和其他业务Claims。顺序反了就容易被表面信息带偏。

4.3 校验签名的正确姿势与算法选择

jwt.io左侧最下方有一个"签名校验"区。HS256时填入共享Secret,如果页面显示"Signature Verified",说明签名匹配;RS256或ES256时需要填入公钥进行验签。

但这里有一个非常关键的误区:jwt.io上的"Signature Verified"只表示签名算法和密钥能对上,并不代表这个Token可以放心使用。它不等于你完成了全部校验。真正的服务端校验至少应该包含:

  1. 签名是否有效(用正确的密钥/公钥验签)。
  2. alg是否在允许列表中,拒绝none算法。
  3. iss是否可信。
  4. aud是否包含当前服务标识。
  5. exp是否已过期,nbf是否已生效。
  6. 必要的jti重放检查。

算法选择上,内部系统用HS256共享密钥简单方便,但密钥分发是隐患;跨系统、面向第三方开放时,用RS256(RSA非对称)更稳妥,私钥签名、公钥验签,公钥可以公开分发。无论哪种,服务端都要固定允许的算法集合,防止算法混淆攻击——攻击者把alg从RS256改成HS256,诱导服务端用公钥当对称密钥来验签,从而自己伪造Token。这类攻击在真实安全报告中多次出现,排查起来也很隐蔽。

5. 自定义Claims与Payload安全红线

5.1 自定义Claims的命名与取值建议

私有声明虽然自由,但命名上我建议遵守几条不成文规矩:

  • 不要和注册声明的7个字段重名,更不要用exp、sub这种名字去表达完全不同的含义。
  • 键名使用小写蛇形或小写驼峰,团队内统一风格。
  • 面向外部开放时,优先用URI命名空间,比如"https://example.com/claims/tenant_id",避免和消费方自己定义的字段冲突。
  • 取值尽量用标量(字符串、数字、布尔值),避免塞入大对象。Claims是给机器看的,不是给你存用户画像用的。

另外,千万别把Claims当成缓存数据库用。我见过有人把用户最近20条订单记录整个塞进Payload,Token直接膨胀到近10KB。客户端每次请求都要携带这串Token,网关、服务端每次都要解码、验签,请求延迟和带宽消耗都上去了。Payload里的Claims应该只放"验证时必须要用"的信息,其他数据要么不传,要么传到服务端后根据sub另行查询。

5.2 Payload不是保险箱:敏感信息红线

这个红线我在第1部分提过,这里再展开说透。由于Payload是Base64URL编码,等于明文,任何拿到Token的人都能解码看到里面所有内容。所以下面这类数据绝对不能放进Payload:

  • 密码、口令、密钥
  • 身份证号、银行卡号
  • 完整的手机号、家庭住址
  • 任何你觉得"不该给别人看"的业务数据

如果业务确实需要保密的Claim数据,正解是用JWE(JSON Web Encryption)对Payload做加密,而不是JWS签名。但JWE实现复杂度更高,大多数业务场景用不上。更常见也更合理的做法是:Token里只放一个不可猜测的引用ID,需要敏感数据时,服务端用这个ID去后端存储里取。这样即便Token泄露,泄露的也只是一个无意义的索引。

5.3 几个看着简单却容易踩的坑

最后把我自己和周围同事实际踩过的坑汇总一下,希望你绕过去。

坑一:改了Payload,忘了重新签名。在jwt.io上调试验签时,手动改了Payload字段,但签名还是旧的,服务端验签直接失败。这在联调阶段非常常见,记住一个原则:任何改动Header或Payload的行为,都必须重新生成完整Token,不是只改其中一段。

坑二:exp和nbf做过期判断时用错了时间源。服务端一定要用服务器时间做校验,不要信任客户端传过来的时间。多实例部署时,各服务器的时钟要同步(比如通过NTP统一校时),否则同样的Token在一台机器上有效、在另一台上就过期了,用户会感觉"时好时坏"。

坑三:拿到了Token就信任它。任何来自网络侧的Token,在验签之前都只是一串不可信的文本。先验签、再校验Claims,顺序不能反。特别是role这类权限字段,如果放在Payload里且服务端没有正确验签就读取,攻击者只要把Token的Payload解开,把"role":"user"改成"role":"admin",再重新打包成一个未签名的Token,就可能绕开权限控制——前提是服务端没有校验签名。

坑四:把HS256的Secret当成RS256的私钥用。HS256要求密钥至少32字节,但很多人用的是24位私钥直接转成HS256密钥,或者反过来。算法和密钥必须配套,否则要么密钥强度不够,要么验签必然失败。

做Token签发和校验时,我个人最后会反复检查三件事:密钥有没有泄露、exp校验有没有真的在服务端执行、Payload里有没有不该出现的敏感字段。这三条守住,大部分JWT相关的线上事故都不会找上门。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询