# 3. 令牌类型与格式:JWT、OAuth令牌与SAML断言

JWT与OAuth令牌接口安全评估 - 第3节

3.1 为什么令牌格式如此重要?

记得我最早做接口安全评估那会儿,看到一个Access 就直接放行了。后来被前辈狠批了一顿——他说:"你知道这串字符里藏着什么吗?"嗯,从那天起我就明白了,令牌格式本身就是最直接的安全防线。

不同令牌有不同的结构、生命周期和验证方式。搞不清楚这些,安全评估就是空中楼阁。这一章咱们就从最常见的三种令牌类型说起。

令牌类型决策树 客户端令牌请求 JWT (JSON Web ) OAuth Access SAML断言 Header (头部) Payload (载荷) Signature (签名) Access (短有效期) Refresh (长有效期) 断言主体 策略与条件 签名与加密 🛡️ 安全评估核心 • JWT: 检查签名算法、防止alg=none攻击 • OAuth: 验证令牌类型、作用域、过期时间 • SAML: 验证断言签名、时间窗口、受众限制

3.2 JWT结构:三段的秘密

说白了,JWT就是一个用点号分隔的三段字符串。你拿到一个JWT,第一眼注意到的就是那两个点。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
dGhpcyBpcyBhIGZha2Ugc2lnbmF0dXJlCg

嗯,你可能觉得这只是一堆乱码。但每个部分都经过Base64URL编码,解码后就清晰了。

3.2.1 Header(头部)

Header就干两件事:声明令牌类型和签名算法。我见过不少新人忽略这个部分,觉得没什么好看的。但安全漏洞往往就藏在这里。

{
  "alg": "RS256",
  "typ": "JWT"
}
⚠️ 危险信号:alg:none

我曾经在渗透测试中遇到过某个系统,Header里的alg直接设成"none"(注意不是算法名称,而是直接取消签名验证)。整个JWT的签名部分被完全绕过了。你想想,任意伪造身份,这得多可怕。

常见算法与安全性对比如下:

算法 对称/非对称 安全等级 评价
HS256 对称 中 密钥需共享,私钥泄露风险高
RS256 非对称 高 我推荐首选,公私钥分离
none - 无 禁止在生产环境使用
ES256 非对称 高 椭圆曲线,性能更好
💡 我的建议

如果你在做安全评估,第一件事就是用工具(比如jwt.io)解码Header。看一眼alg字段,如果看到"none"、"None"、"NoSignature"之类的东西——直接亮红灯。

3.2.2 Payload(载荷)

Payload就是令牌的核心数据。它包含一组"声明"(Claims)。说白了就是你告诉对方你是谁、有什么权限、什么时候过期。

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022,
  "exp": 1516242622,
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com"
}

这里要注意,JWT的Payload是Base64URL编码的,不是加密的。什么意思?任何拿到令牌的人都能解码看内容。所以千万别往里面放密码、信用卡号这种敏感数据。

📋 标准声明字段说明
  • iss (Issuer): 签发者 — 验证令牌来源是否可信
  • sub (Subject): 主体 — 用户的唯一标识
  • aud (Audience): 受众 — 令牌目标服务(防止令牌被重用)
  • exp (Expiration): 过期时间 — 务必验证当前时间是否在exp之前
  • nbf (Not Before): 生效时间 — 令牌在nbf之前无效
  • iat (Issued At): 签发时间 — 辅助审计
  • jti (JWT ID): 唯一标识 — 防止重放攻击

3.2.3 Signature(签名)

签名是整个JWT的安全底线。它的生成过程是这样的:

signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

或者用非对称算法时:

signature = RSASHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey
)

客户端发来JWT,服务端用同样的密钥(对称)或公钥(非对称)重新计算签名。如果计算结果不一致,直接拒绝。你想想看,如果有人篡改了Payload里的"admin": false改成"admin": true,签名肯定对不上,攻击就失败了。

⚠️ 签名算法混淆攻击

我曾经见过一个案例,服务端用RS256验证,但客户端把Header的alg改成HS256,然后拿服务端的公钥当对称密钥来伪造签名。听起来绕?其实就是利用了服务端验证逻辑不够严谨。正确的做法是:必须在校验签名时固定允许的算法列表。

3.3 OAuth Access 与 Refresh

OAuth 2.0协议定义了两种核心令牌:Access 和Refresh 。很多人搞混它们的职责。

3.3.1 Access (访问令牌)

Access 是客户端用来访问受保护资源的凭证。它有效期很短——常见的15分钟到1小时。为什么?因为短生命周期可以最大限度降低泄露风险。

Access 的格式可以是任何一种:JWT、SAML、甚至是一个随机字符串(当服务端需要回查时)。在安全评估时,我第一步就是看Access 的过期时间。

属性 Access Refresh
生命周期 短(15分钟~1小时) 长(数小时~数月)
存储位置 内存或安全Cookie 后端或安全存储(如Keychain)
传输频率 每次API请求都携带 仅在令牌刷新时发送
泄露影响 短期可控 高危,需立即撤销

3.3.2 Refresh (刷新令牌)

Refresh 的作用就一个:在Access 过期后,无痛地换取一个新的Access 。它不直接访问资源,但它能"造"出新的Access ,所以它的保护级别要比Access 高得多。

我个人习惯在安全评估中重点检查以下几点:

  1. Refresh 是否绑定——有没有和特定的客户端IP或设备ID做绑定?
  2. 是否支持撤销——用户主动登出时,Refresh 是否被立即失效?
  3. 轮换策略——我建议每次使用Refresh 后都返回一个新,旧立即作废。这样可以防止泄露后被盗用。
📌 令牌轮换实践

我参与的一个项目中,我们实现了Refresh 的轮换。每次客户端用Refresh 换取新Access 时,服务端都返回一个新的Refresh ,旧的那个直接失效。这样就算某个Refresh 被截获,它也只能用一次。

3.4 SAML断言:XML格式的令牌

SAML断言和JWT不一样——它用XML格式,通常用在企业级单点登录(SSO)场景。你可能会觉得XML有点过时了,但很多政府、金融机构的系统还在用。

<?xml version="1.0" encoding="UTF-8"?>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                ID="_123456789"
                IssueInstant="2023-11-01T10:00:00Z"
                Version="2.0">
  <saml:Issuer>https://idp.example.com</saml:Issuer>
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <ds:SignedInfo>
      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
    </ds:SignedInfo>
    <ds:SignatureValue>...</ds:SignatureValue>
  </ds:Signature>
  <saml:Subject>
    <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
      john.doe@example.com
    </saml:NameID>
    <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
      <saml:SubjectConfirmationData NotOnOrAfter="2023-11-01T10:10:00Z"
                                     Recipient="https://sp.example.com/acs"/>
    </saml:SubjectConfirmation>
  </saml:Subject>
  <saml:Conditions NotBefore="2023-11-01T09:55:00Z"
                   NotOnOrAfter="2023-11-01T10:10:00Z">
    <saml:AudienceRestriction>
      <saml:Audience>https://sp.example.com</saml:Audience>
    </saml:AudienceRestriction>
  </saml:Conditions>
  <saml:AuthnStatement AuthnInstant="2023-11-01T09:59:00Z">
    <saml:AuthnContext>
      <saml:AuthnContextClassRef>
        urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
      </saml:AuthnContextClassRef>
    </saml:AuthnContext>
  </saml:AuthnStatement>
</saml:Assertion>

做SAML安全评估时,我最关注三个要素:

  • 签名验证:XML签名有没有被验证?我曾经遇到的案例里,服务端只验证了签名是否存在,却没验证签名是否有效。攻击者就伪造带任意签名的断言通过了。
  • 时间窗口:NotBefore和NotOnOrAfter决定了断言的有效期。永远要检查当前时间是否在这个窗口内。
  • 受众限制(Audience Restriction):断言指定了它要发给哪个服务提供方(SP)。如果服务端不检查这个字段,一个发给A系统的断言就能直接用在B系统上。

3.5 三种令牌格式对比总结

好,咱们把三种令牌放一起做个对比,方便你心里有个全景图。

特性 JWT OAuth Access SAML断言
格式 JSON(Base64URL编码) 不固定(常用JWT或opaque) XML
自包含性 ✅ 是,Payload包含所有信息 取决于实现 ✅ 是,断言包含完整内容
加密方式 签名(JWS)或加密(JWE) 通常不加密,靠HTTPS 支持签名和加密
典型应用 API认证、分布式系统 OAuth 2.0 + OIDC 企业SSO、联邦身份
安全评估重点 算法、签名、过期时间 作用域、生命周期、撤销 签名验证、时间窗口、受众

嗯,说到这里,我想强调一个容易被忽略的点:不管用什么格式的令牌,**解析和验证永远要放在服务端逻辑的最前面**。别让客户端来决定令牌的格式、算法甚至内容。我在审计中见过太多"先信任再验证"的案例——结果就是各种漏洞。