JWT 简介、陷阱及建议

Table of Contents

JSON Web Token (JWT, 发音为 "jot") 是一个附带签名的字符串,常常作为服务调用的令牌。作为令牌时,由于 JWT 可以附带 Session 信息,可以简化服务逻辑,在微服务结构盛行的今天应用广泛。互联网上单点登录和身份管理的通用标准 OpenID Connect 也使用 JWT 作为身份令牌。

1 JWT 解决的问题, 之一

以一个简单的结构为例。如下图所示,客户端需要从三个服务获取资源,三个服务怎么判断客户端是否有访问权限呢?

jwt-exp-1.png

(1) 使用共享存储保存会话状态和权限

其中一个办法,是使用一块共享存储,保存身份和状态信息。客户端从认证服务获得权限和令牌后,再携带令牌请求服务A,B,C。认证服务在认证时将客户端令牌和权限信息写入共享存储,服务A,B,C收到客户端请求时,使用请求中携带的令牌查询共享内存,判断权限。如下图所示,认证服务授予了客户端服务A,B的访问权限,但没有授予C的访问权限,客户端请求服务A,B时,服务A,B查询共享内存确认权限,予以回复; 客户端请求服务C时,服务C从共享内存查询权限,发现客户端没有授予C的访问权限,遂拒绝客户端的访问请求。

jwt-exp-1.2.png

然而这个方案并不适用于微服务盛行的今天。多个团队协商共享存储的方案,使用的格式,并不容易。

(2) 认证的判定都通过认证服务

客户端仍然从认证服务获取令牌,每个服务收到客户端请求后,都询问一遍认证服务,是否允许携带此令牌的客户端访问本服务,获取其求的资源,收到认证服务答复后再执行客户端的请求。

jwt-exp-1.3.png

这是典型的微服务方案,但需要频繁调用认证服务,也有单点故障的问题。

(3) 使用 JWT

若使用JWT,每次请求服务A,B,C时,携带的JWT令牌都包含了客户端的权限信息,服务A,B,C 从令牌中直接获得客户端的访问权限,就不需要再去其他系统查询了。

jwt-exp-1.4.png

2 JWT 的格式

一个 JWT 看起来像是下面这个样子的。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiY2RlZjEyMzQ1Njc4OTAifQ.eyJpc3MiOiIxMjM0NTYtY29tcHV0ZUBkZXZlbG9wZXIuZ3NlcnZpY2VhY2NvdW50LmNvbSIsInN1YiI6IjEyMzQ1Ni1jb21wdXRlQGRldmVsb3Blci5nc2VydmljZWFjY291bnQuY29tIiwiYXVkIjoiaHR0cHM6Ly9maXJlc3RvcmUuZ29vZ2xlYXBpcy5jb20vIiwiaWF0IjoxNTExOTAwMDAwLCJleHAiOjE1MTE5MDM2MDB9. MHMyGEpXlH62xdIbNXipWUlz5lGSRpCjaOzZPG5L3AWwOAiZoETiOP_tZJp7IfmA7QmfiHHQ8C7Dwwq3-bLsVzvBnkbkXAyv66x8gdvIljs6GzRqg7JJBc31IHAlWu5LnRmpqGKZktxD67rR0gpxL2pEy_pt2pSaUprlC-6Q9wmobK8Xe_7wotugPzD118Q9_ZMzLchegPxlVrjFGsFv7KP1yIepKvZ12Art1vxzb7E-9LlntBk9lgfQsqPtoNGDoYe4HPxyU6Iqa2savpsSREeSEJKCRgyqUoWrH19zLlDPEuBHRTZHmL-PdmsPxsYVZ3VJ0_ZXzmy9KeiIZ23-dQ

为了清晰显示三部分,使用三种颜色标记出来。三个部分是 base64url 编码的,使用 "." 将三部分连接起来,形成 JWT。 在jwt.io可以将其解析为三部分,如下图。

jwt.io-exp.png

三个部分分别为 HEADER, PAYLOAD, SIGNATURE。如图所示,HEADER 和 PAYLOAD 两部分解码后是 JSON 格式的。实际上,他们编是将 JSON 字符串经过 base64url 编码后所得。而签名部分是将前两部分,使用指定的签名算法和密钥,签名后经过 base64url 编码后所得。下面详细说明。

2.1 HEAER

HEADER 部分是 JSON 格式的数据,经过 base64url 编码后所得。HEADER 通常包含 typ和 alg,大多数时候也会包含 kid。详细内容参考 JWSJWE 。这里只介绍最常用的字段和值。

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "abcdef1234567890"
}

(1) typ

表示内容类型。既然是JWT,就一直填写 "JWT" 值就可以了。注意大写。大写不是 标准 要求的,但是有些JWT实现只认大写。

(2) alg

表示签名字段(第三个部分)使用何种签名算法,具体dinginess在 JWA

+--------------+-------------------------------+--------------------+
| "alg" Param  | Digital Signature or MAC      | Implementation     |
| Value        | Algorithm                     | Requirements       |
+--------------+-------------------------------+--------------------+
| HS256        | HMAC using SHA-256            | Required           |
| HS384        | HMAC using SHA-384            | Optional           |
| HS512        | HMAC using SHA-512            | Optional           |
| RS256        | RSASSA-PKCS1-v1_5 using       | Recommended        |
|              | SHA-256                       |                    |

| RS384        | RSASSA-PKCS1-v1_5 using       | Optional           |
|              | SHA-384                       |                    |
| RS512        | RSASSA-PKCS1-v1_5 using       | Optional           |
|              | SHA-512                       |                    |
| ES256        | ECDSA using P-256 and SHA-256 | Recommended+       |
| ES384        | ECDSA using P-384 and SHA-384 | Optional           |
| ES512        | ECDSA using P-521 and SHA-512 | Optional           |
| PS256        | RSASSA-PSS using SHA-256 and  | Optional           |
|              | MGF1 with SHA-256             |                    |
| PS384        | RSASSA-PSS using SHA-384 and  | Optional           |
|              | MGF1 with SHA-384             |                    |
| PS512        | RSASSA-PSS using SHA-512 and  | Optional           |
|              | MGF1 with SHA-512             |                    |

| none         | No digital signature or MAC   | Optional           |
|              | performed                     |                    |
+--------------+-------------------------------+--------------------+

签名算法分为3类,第一类是 "HS" 开头的,使用对称密钥,HMAC算法进行签名和验证;第二类是 "RS/ES/PS" 开头的,使用非对称密钥进行签名和验证;第三类是 none,不签名,一般 alg 不设置为 none。

(3) kid

表示密钥ID,方便验证端选择验证签名的密钥。很多系统不使用这个字段,

2.2 PAYLOAD

PAYLOAD 也称作 Claims, JSON 格式的数据,经过 base64url 编码后所得。可以填入任何需要的值。这里只介绍最常用的字段,这些常用字段都是可选的,可以不设置。

{
  "iss": "123456-compute@developer.gserviceaccount.com",
  "sub": "123456-compute@developer.gserviceaccount.com",
  "aud": "https://firestore.googleapis.com/",
  "exp": 1511903600
}

(1) 签发者谁签发的这个 JWT。经常是一段网址,如 "https://login.example.com/" ,也有很多系统使用字符串。具体怎么标识签发者由系统实现时自行决定。

(2) sub 签发给谁。

(3) aud 这个 JWT 将用来请求哪个服务。

(4) exp 表示这个 JWT 的超时时间,超过这个时间则不再被系统承认。

2.3 签名

将前面所述的两部分使用 "." 连接后,签名并 base64url 所得。签名算法列表如前所述。

签名保证了数据数据不可伪造补课修改,但数据仍然是明文的。

HMACSHA256(
  base64UrlEncode(HEADER) + "." +
  base64UrlEncode(PAYLOAD),
  secret)

3 陷阱和建议

3.1 alg 字段的问题

使用 JWT 作为令牌,签名肯定是必要的。但是表示签名算法的 alg 字段,明文写在 HEADER 里。这是公认的标准设计问题,alg 字段本不该存在。

目前已知的攻击方式至少有两种:

(1) 将 alg 字段改为 "none" 攻击者按照格式构造一个JWT,并将 alg 字段设置为 none。如果验证方按照 JWT 的 alg 字段指定的算法验证 JWT,发现 alg 为 none,就直接通过验证了。

(2) 将非对称签名算法改为对称签名算法例如,将 alg="RS256" 改为 alg="HS256"。 攻击者截获一个 RS256 签名方式的 JWT,按照其格式构造并修改,设置 alg="SHA256",并使用公钥作为签名密钥签名。正常情况下,验证方按照 alg 字段指定的算法验证 JWT,发现 alg="RS256",使用公钥按照 RS256 算法验证;被使用次方式攻击时,发现 alg="HS256",则使用 HMAC-SHA256 算法,使用RSA的公钥作为签名密钥验证。公钥一般是公开的,所以这个构造是可能的。

3.2 对称密钥太短

对称密钥太短或者太有规律,就跟使用弱密码一样,有很多这方面的 破解 工具

3.3 明文

PAYLOAD 是明文的,千万别把敏感信息写在 JWT 里。见过把用户的密码写在 JWT 里,被人拿到就泄露了; 也有把用户信息如手机号身份证号写在 JWT 里的,违反了大多数国家的法律法规。

当然 JWT 也有 加密方案,只是不常用。

3.4 SQL 注入或类似的

这与具体实现的系统有关。如果 HEADER.kid="../../../../../../../dev/null",你的系统是不是使用空的密钥验证签名呢? 如果 HEADER.kid="xxx' UNION SELECT 'aaa",你的系统会不会使用 "aaa" 作为密钥验证签名呢?

3.5 使用建议

微软专门写了个 JSON Web Token Best Current Practices,照做就可以。当然还有其它注意事项:

(1) 防止 XSS, 如果在 cookie 中使用 JWT,使用 HttpOnly 标志。

(2) 别存 JWT。

(3) 不要相信并依赖 JWT 中的字段,验证字段与期望是否匹配。特别注意验证 alg 字段的值是否跟要求的一致。

(4) 对称签名密钥够长,至少跟哈希算法长度一致。

(5) token 只是 token,别用来管理 Session。

(6) 不要把敏感信息写在 JWT 里。


By .