以实例说明 OAuth2
OAuth2 授权协议详解:单点登录、第三方授权的实现原理与实战案例
JSON Web Token (JWT, 发音为 "jot") 是一个附带签名的字符串,常常作为服务调用的令牌。作为令牌时,由于 JWT 可以附带 Session 信息,可以简化服务逻辑,在微服务结构盛行的今天应用广泛。互联网上单点登录和身份管理的通用标准 OpenID Connect 也使用 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 从令牌中直接获得客户端的访问权限,就不需要再去其他系统查询了。
一个 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 编码后所得。下面详细说明。
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,方便验证端选择验证签名的密钥。很多系统不使用这个字段,
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 的超时时间,超过这个时间则不再被系统承认。
将前面所述的两部分使用 "." 连接后,签名并 base64url 所得。签名算法列表如前所述。
签名保证了数据数据不可伪造补课修改,但数据仍然是明文的。
HMACSHA256(base64UrlEncode(HEADER) + "." + base64UrlEncode(PAYLOAD), secret)
使用 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的公钥作为签名密钥验证。公钥一般是公开的,所以这个构造是可能的。
PAYLOAD 是明文的,千万别把敏感信息写在 JWT 里。见过把用户的密码写在 JWT 里,被人拿到就泄露了; 也有把用户信息如手机号身份证号写在 JWT 里的,违反了大多数国家的法律法规。
当然 JWT 也有 加密方案,只是不常用。
这与具体实现的系统有关。如果 HEADER.kid="../../../../../../../dev/null",你的系统是不是使用空的密钥验证签名呢? 如果 HEADER.kid="xxx' UNION SELECT 'aaa",你的系统会不会使用 "aaa" 作为密钥验证签名呢?
微软专门写了个 JSON Web Token Best Current Practices,照做就可以。当然还有其它注意事项:
(1) 防止 XSS, 如果在 cookie 中使用 JWT,使用 HttpOnly 标志。
(2) 别存 JWT。
(3) 不要相信并依赖 JWT 中的字段,验证字段与期望是否匹配。特别注意验证 alg 字段的值是否跟要求的一致。
(4) 对称签名密钥够长,至少跟哈希算法长度一致。
(5) token 只是 token,别用来管理 Session。
(6) 不要把敏感信息写在 JWT 里。
By Liao Tonglang.
把当前热点继续串成多页阅读,而不是停在单篇消费。
OAuth2 授权协议详解:单点登录、第三方授权的实现原理与实战案例
PKCE 安全增强详解:OAuth2 授权码流程的安全加固方案
密码安全存储最佳实践:哈希算法、加盐、bcrypt/scrypt/Argon2 使用指南
XSS 跨站脚本攻击实战:攻击原理、防御方法与安全编码最佳实践