JWT 简介、陷阱及建议
Table of Contents
JSON Web Token (JWT, 发音为 "jot") 是一个附带签名的字符串,常常作为服务调用的令牌。作为令牌时,由于 JWT 可以附带 Session 信息,可以简化服务逻辑,在微服务结构盛行的今天应用广泛。互联网上单点登录和身份管理的通用标准 OpenID Connect 也使用 JWT 作为身份令牌。
1 JWT 解决的问题, 之一
以一个简单的结构为例。如下图所示,客户端需要从三个服务获取资源,三个服务怎么判断客户端是否有访问权限呢?
(1) 使用共享存储保存会话状态和权限
其中一个办法,是使用一块共享存储,保存身份和状态信息。客户端从认证服务获得权限和令牌后,再携带令牌请求服务A,B,C。认证服务在认证时将客户端令牌和权限信息写入共享存储,服务A,B,C收到客户端请求时,使用请求中携带的令牌查询共享内存,判断权限。如下图所示,认证服务授予了客户端服务A,B的访问权限,但没有授予C的访问权限,客户端请求服务A,B时,服务A,B查询共享内存确认权限,予以回复; 客户端请求服务C时,服务C从共享内存查询权限,发现客户端没有授予C的访问权限,遂拒绝客户端的访问请求。
然而这个方案并不适用于微服务盛行的今天。多个团队协商共享存储的方案,使用的格式,并不容易。
(2) 认证的判定都通过认证服务
客户端仍然从认证服务获取令牌,每个服务收到客户端请求后,都询问一遍认证服务,是否允许携带此令牌的客户端访问本服务,获取其求的资源,收到认证服务答复后再执行客户端的请求。
这是典型的微服务方案,但需要频繁调用认证服务,也有单点故障的问题。
(3) 使用 JWT
若使用JWT,每次请求服务A,B,C时,携带的JWT令牌都包含了客户端的权限信息,服务A,B,C 从令牌中直接获得客户端的访问权限,就不需要再去其他系统查询了。
2 JWT 的格式
一个 JWT 看起来像是下面这个样子的。
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiY2RlZjEyMzQ1Njc4OTAifQ.eyJpc3MiOiIxMjM0NTYtY29tcHV0ZUBkZXZlbG9wZXIuZ3NlcnZpY2VhY2NvdW50LmNvbSIsInN1YiI6IjEyMzQ1Ni1jb21wdXRlQGRldmVsb3Blci5nc2VydmljZWFjY291bnQuY29tIiwiYXVkIjoiaHR0cHM6Ly9maXJlc3RvcmUuZ29vZ2xlYXBpcy5jb20vIiwiaWF0IjoxNTExOTAwMDAwLCJleHAiOjE1MTE5MDM2MDB9. MHMyGEpXlH62xdIbNXipWUlz5lGSRpCjaOzZPG5L3AWwOAiZoETiOP_tZJp7IfmA7QmfiHHQ8C7Dwwq3-bLsVzvBnkbkXAyv66x8gdvIljs6GzRqg7JJBc31IHAlWu5LnRmpqGKZktxD67rR0gpxL2pEy_pt2pSaUprlC-6Q9wmobK8Xe_7wotugPzD118Q9_ZMzLchegPxlVrjFGsFv7KP1yIepKvZ12Art1vxzb7E-9LlntBk9lgfQsqPtoNGDoYe4HPxyU6Iqa2savpsSREeSEJKCRgyqUoWrH19zLlDPEuBHRTZHmL-PdmsPxsYVZ3VJ0_ZXzmy9KeiIZ23-dQ
为了清晰显示三部分,使用三种颜色标记出来。三个部分是 base64url 编码的,使用 "." 将三部分连接起来,形成 JWT。 在jwt.io可以将其解析为三部分,如下图。
三个部分分别为 HEADER, PAYLOAD, SIGNATURE。如图所示,HEADER 和 PAYLOAD 两部分解码后是 JSON 格式的。实际上,他们编是将 JSON 字符串经过 base64url 编码后所得。而签名部分是将前两部分,使用指定的签名算法和密钥,签名后经过 base64url 编码后所得。下面详细说明。
2.1 HEAER
HEADER 部分是 JSON 格式的数据,经过 base64url 编码后所得。HEADER 通常包含 typ和 alg,大多数时候也会包含 kid。详细内容参考 JWS 和 JWE 。这里只介绍最常用的字段和值。
{ "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.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 里。