浅析 JWT
JSON Web Token,简称 JWT,读音是 [dʒɒt]( jot 的发音),是一种当下比较流行的「跨域认证解决方案」。 注意它是一套 RFC 规范,相关的还有 JWE/JWS/JWK/JOSE。 它有很多优点,也有局限性,但我们可以配合其他方案做出适合自己业务的一套方案。 本篇是对 JWT 做一个简单的介绍和简单实践总结。
JSON Web Token (JWT) is a compact claims representation format intended for space constrained environments such as HTTP Authorization headers and URI query parameters.
JWT的组成
JWT 由三部分组成:头部、数据体、签名/加密。
这三部分以 . (英文句号)连接,注意这三部分顺序是固定的,即 header.payload.signature ,如下示例:
1. 头部 The Header
这部分用来描述 JWT 的元数据,比如该 JWT 所使用的签名/加密算法、媒体类型等。
这部分原始数据是一个JSON对象,经过Base64Url编码方式进行编码后得到最终的字符串。其中只有一个属性是必要的: alg ——加密/签名算法,默认值为 HS256 。
最简单的头部可以表示成这样:
其他 可选 属性:
– typ ,描述 JWT 的媒体类型,该属性的值只能是 JWT ,它的作用是与其他 JOSE Header 混合时表明自己身份的一个参数(很少用到)。
– cty ,描述 JWT 的内容类型。只有当需要一个 Nested JWT 时,才需要该属性,且值必须是 JWT 。
– kid ,KeyID,用于提示是哪个密钥参与加密。
Base64url 编码是 Base64 的一种针对 URL 的特定变种。因为 = 、+、/ 这个三个字符在 URL 中是有特定含义的,所以 Base64url 分别将 = 直接忽略,+ 替换成 -,/ 替换成 _
2. 数据体 The Payload
这部分用来描述JWT的内容数据,即存放些什么。
原始数据仍是一个 JSON 对象,经过 Base64url 编码方式进行编码后得到最终的 Payload。这里的数据默认是不加密的,所以不应存放重要数据(当然你可以考虑使用嵌套型 JWT)。官方内置了七个属性, 大小写敏感 ,且都是可选属性,如下:
– iss (Issuer) 签发人,即签发该 Token 的主体
– sub (Subject) 主题,即描述该 Token 的用途
– aud (Audience) 作用域,即描述这个 Token 是给谁用的,多个的情况下该属性值为一个字符串数组,单个则为一个字符串
– exp (Expiration Time) 过期时间,即描述该 Token 在何时失效
– nbf (Not Before) 生效时间,即描述该 Token 在何时生效
– iat (Issued At) 签发时间,即描述该 Token 在何时被签发的
– jti (JWT ID) 唯一标识
除了这几个内置属性,我们也可以自定义其他属性,自由度非常大。
这里对 aud 做一个说明,有如下 Payload:
那么如果我拿这个 JWT 去 http://www.c.com 获取有访问权限的资源,就会被拒绝掉,因为 aud 属性明确了这个 Token 是无权访问 www.c.com 的,有同学会说这部分反正不加密,那我本地把 www.c.com 加入进去不就完事了。别急,下面这部分看完先。
3. 签名/加密 The signature/encryption data
这部分是相对比较复杂的,因为 JWT 必须符合 JWS/JWE 这两个规范之一,所以针对这部分的数据如何得来就有两种方式,我们先来看一个简单的例子,有如下 JWT:
对前两部分用 Base64url 解码后能得出相应原始数据,
Header 部分:
Payload 部分:
根据 Header 部分的 alg 属性我们可以知道该 JWT 符合 JWS 中的规范,且签名算法是 HS256 也就是 HMAC SHA-256 算法,那么我们就可以根据如下公式计算最后的签名部分:
其中的密钥是保证签名安全性的关键,所以必须保存好,在本例中密钥是 123456。 因为有这个密钥的存在,所以即便调用方偷偷的修改了前两部分的内容,在验证环节就会出现签名不一致的情况,所以保证了安全性。
在实现过程中,遇到了这样一个问题:如果使用 RS256 这类非对称加密算法,加密出来的是一串二进制数据,所以第三部分还是用 Base64 编码了一层,这样最终的 JWT 就是可读的了。
为什么用它
1. Stateless 无状态 ,一方面可以有效减少服务端保存 Session 的负载;另一方面可以方便的进行扩平台的横向扩展,如 SSO 单点授权。
2. 可以有效携带 必要但不敏感 的信息,且是 JSON 这种非常通用的格式。
一般我们都拿它和传统的基于 Session-Cookie 的管理方式进行大致比较。
传统的基于 Session 的会话管理逻辑大致如下时序图所示:
相比较而言,传统的 Session-Cookie 方式会有几点问题:
1. 频繁查找 Session 的开销过大。 因为 Session 存储在服务端,大部分接口的请求都需要查找 Session 以获取对应用户身份。不管是存储在持久层(数据库)还是内存中,频繁查找带来的压力会随着用户量的上升而急剧增大。
2. 不支持跨域,可扩展性差。 举个例子:假设网站A和网站B的用户数据是共享的,当用户在网站A上登录以后,我们希望在访问网站B时也保持登录状态。这个时候就会出现一个情况:生成这个 SessionID 的服务器并不是验证这个 SessionID 的服务器,也就出现了跨域身份认证的问题。除非我们把身份认证的数据也共享,即将 Session 放在持久层单独存储,统一管理,这样就能在多域名下共享了,但是这样做的成本有点高。
3. 安全性较差。 Session 放在 Cookie 中容易被 CSRF 攻击,而且在多域名的业务场景下需要额外的做兼容性处理,容易出现安全漏洞。
Security
1. 因为 JWT 的前两个部分仅是做了 Base64 编码处理并非加密,所以在存放数据上不能存放敏感数据。
2. 用来签名/加密的密钥需要妥善保存。
3. 尽可能采用 HTTPS,确保不被窃听。
4. 如果存放在 Cookie 中则强烈建议开启 Http Only,其实官方推荐是放在 LocalStorage 里,然后通过 Header 头进行传递。
Cookie 的 HTTP Only 这个 Flag 和 HTTPS 并不冲突,你会发现其实还有一个 Secure 的 Flag,这个就是指 HTTPS 了,这两个 Flag 互不影响的,开启 HTTP Only 会导致前端 JavaScript 无法读取该 Cookie,更多的是为了防止 类 XSS 攻击。
一些问题和思考
JWT 的缺点其实也蛮多的,适不适用得具体看业务场景,哪个优势更大用哪个。(一点感悟:在写这篇文章前一直是 JWT 的坚定拥护者,越写越发现其实传统的 Session-Cookie 方案挺好的,很成熟。它们两者都有优缺点,选型上要多思考斟酌才行。)
1. 数据臃肿
因为 payload 只是用 Base64 编码,所以一旦存放数据大了,编码之后 JWT 会很长,cookie 很可能放不下,所以还是建议放 LocalStorage,但是每次 HTTP 请求都带上这个 臃肿的 Header 开销也随之变大 。
2. 无法废弃和续签
1. 如果有效期设置过长,意味着这个 Token 泄漏后可以被长期利用,危害较大,所以一般我们都会设置一个较短的有效期。由于有效期较短,意味着需要经常进行 重新授权 的操作。
2. 假设在用户操作过程中升级/变更了某些权限,势必需要 刷新 以更新数据。
要解决这个问题,需要在服务端部署额外逻辑,常见的做法是增加刷新机制和黑名单机制,通过 Refresh Token 刷新 JWT,将需要废弃的 Token 加入到黑名单。
你的在看,我都认真当成了喜欢