对 Session、Token、JWT 认证的碎碎念

Published: 2023-02-28

Tags: 笔记

本文总阅读量

一直都没有梳理过基于 Web 服务 Session / Token 认证方式,刚接触计算机那会儿服务端渲染很流行,当时看到关于 Session 相关的知识感觉云里雾里,现今,认证有了更多的方式和选择,值得梳理和研究。本篇博文整理了相关知识,尽量保证准确。

基于 Cookie 的 Session 认证方式

用户向购物车添加了商品,而后点击下单,因为 HTTP 请求是无状态的,所以服务端无法判断两个请求是来自于一个用户。

为了解决这个问题,用户首次访问服务端页面时,服务端会为用户生成 session,并将 session 保存在服务器中,然后将 session 的唯一标识 SessionId 通过响应返回给浏览器。

通常浏览器支持服务端使用 Set-Cookie 方法将 SessionId 存入客户端的 Cookie,浏览器会在下次请求时自动携带 Cookie,这样服务端通过解析 Cookie 中的 SessionId 就能将用户一连串的访问关联起来。

Cookie 就像是浏览器提供给用户的小甜点,它支持存入不超过 4kb 的键值类型文本数据,通常存储用户相关信息,它支持通过 expries 属性来设置 cookie 是否随浏览器关闭而失效。

Cookie 典型的场景的就是购物网站加购物车,即使用户没有登录,添加商品到购物车,关闭浏览器再打开购物车,商品往往还在,就是通过 Cookie 存储了加购商品信息,而下单前的登录,服务器会把 SessionId 和 UserId 关联起来。

而基于 Cookie 实现的 Session 认证,就是将 SessionId 存入了 Cookie,未登录时看起来是一个匿名用户,登陆后通过 SessionId 就能唯一确定用户,另外如果 session 失效,用户需要重新登录来创建一个新的会话。

Session 更准确的说是一个客户端与服务端的“会话状态”,在 HTTP 无状态的服务上维持着有状态的数据,进而通过 Session 的具体实现可以对用户进行认证。

优势

  • 服务端能够即时的让 Session 失效,停止响应客户端。

劣势

  • 通常 Session 存储在内存,重启服务会导致 Session 丢失,用户需要重新登录。
  • 单机性能有限难以支持大量用户,集群模式下又需要考虑 Session 共享问题。
  • Cookie 仅浏览器支持,移动端等其它客户端没有 Cookie 的实现。

实践

基于 Session 服务 Web 服务,单机通常将 Session 存入 Memcached 等服务,避免存储在内存重启服务后 Session 丢失,而在集群模式下可以使用 Redis 存储 Session,或是通过 Nginx 将相同 Session 根据规则转发到同一机器处理,都能达到共享 Session 的目的。

后者同样会有一些问题,Nginx 转发 Session 的方式会限制用户只能访问一台机器,在机器遇到故障或需要动态扩容时,Session 同步的问题就又会出现。

另外 Session 也可以存储在数据库,好处是避免 Session 丢失,搭配缓存使用,性能和稳定性都不错,同样带来的问题是需要确保数据库缓存状态一致,频繁读写也会对数据库造成一定的压力。

关于服务器 Session 信息的存储及使用,不同服务多少都有自己的考量和侧重,难以获得最佳实践,而随着Web 前后端的分离,客户端的多样性发展,统一认证的需求,使得认证的方式也发生着改变。

—— Token,逐渐走进了人们的视野,成为时代的主旋律。

Token 与 Session

Token 的概念来自于 OAuth 2.0,这是一个标准化项目。

OAuth 2.0 最初是由 OAuth 社区组织在 2007 年启动的一个开放标准化协议项目,目的是为了提供一种统一的,安全的和可扩展的授权协议。OAuth 2.0 的首个草案版本于 2010 年发布,最终规范版本则于2012年正式发布。自发布以来,OAuth 2.0 已经成为了互联网领域中应用最为广泛的一种认证和授权协议,被广泛地应用于各种场景中,例如第三方登录、API 访问授权、资源共享等。

OAuth2.0 的授权简单理解其实就是获取令牌(token)的过程,OAuth 协议定义了四种获得令牌的授权方式(authorization grant ):授权码(authorization-code)、简单式(implicit)、密码式(password)、客户端凭证(client credentials),一般常用的是授权码和密码模式。

当下的互联网,访问量大的服务不可能采用单台机器 All In One(All In Boom) 的方案,即用户的认证、资源、API 服务都是由不同的基础服务提供的,基于 Cookie 的 Session 方案在面临跨域访问时就会捉襟见肘。

关于跨域的补充:浏览器访问 a.com 会携带 a.com 的 Cookie,访问 b.com 会携带 b.com 的 Cookie,当想在访问 a.com 的时候请求到 b.com 的资源,正常情况下是获取不到 b.com 网站 Cookie 的,浏览器出于安全策略,禁止请求。

而使用 Token 可以避免跨域的原因是 Token 通过 Header 而不是 Cookie 传到服务器。

认证成功后,服务端可以通过 Set-Cookie 方法将 Token 存入浏览器的 Cookie,当客户端需要使用 b.com 的资源时,客户端从 a.com 的 Cookie 获取到 Token,补充到 Header 上,就能从根本上避免跨域问题。(为了 Token 的安全,应设置 HTTP-Only,且全程使用 HTTPS,另外除了 Cookie,Token 也可以存储在浏览器的 localStorage)

OAuth2.0 定义了标准化的认证模式,而在具体的实现方案上,不同的服务会有自己的模式。

  • 例如一个小服务,只有一套 API,那么 login 接口就是获取 Token 的过程,而后请求都携带这个 Token,实现鉴权。
  • 如果一个服务需要依赖 Github 登录,客户端需要先向 Github 发起授权请求,Github 会引导用户登录并授权,授权完成后Github 将授权码返回给客户端,客户端再通过授权码向 Github 申请访问令牌(Token),最终客户端使用访问令牌访问用户的资源。

以下是 Github 返回的访问令牌信息,其中有 access_token、refresh_token,后续会进一步了解这两种 Token 的用途。

{
  "access_token": "e72e16c7e42f292c6912e7710c838347ae178b4a",
  "token_type": "bearer",
  "scope": "user",
  "expires_in": 3600,
  "refresh_token": "r1.a526e1f10...",
  "refresh_token_expires_in": 5184000
}

Bearer Token 是什么?

GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mFa9dB5fv4n1JqM

在我们使用第三方 API 时,可能会看到 Header 格式为 “Authorization: Bearer {Token}”,这里的 Bearer 字符串表示后边儿跟着一个访问令牌,最开始我不了解这个关键字是协议中的规定,还是少数服务的实现。

查阅后得知它是 RFC6750:The OAuth 2.0 Authorization Framework: Bearer Token Usage 中提供的范例,因为 OAuth 2.0 没有明确说明 「Token 应该长什么样子」,正如 HTTP 基本的 Basic 认证格式一样。

GET / HTTP/1.1 
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

服务端在开发时也应尽量遵守这一约定,表明是 Bearer 类型的 Token,这是推荐方式,当然也可以灵活的指定其它 Header 名,或传递加密后的 Token,前后端的使用保持一致即可。

这篇博客整理了 Bearer Token 的更多内容:OAuth 2.0 筆記 (6) Bearer Token 的使用方法(推荐阅读)

Access Token 与 Refresh Token

上例提到的 Github 返回信息,Access Token 用于请求资源,Refresh Token 用于获取新的 Access Token。

一般 Access Token 的有效期很短,几分钟到几十分钟不等,Refresh Token 相对较长,可能是几天或数周。

客户端应该对获取 Access Token 的逻辑进行封装,使其通过 Refresh Token 更新 Access Token 的行为是原子操作。

当服务器返回 Access Token 失效,此时应调用 Refresh Token 获取新的 Access Token,而后再次请求接口,这个过程应是用户是无感知的。

问题来了,如果将 Access Token 的有效期设置为 Refresh Token,是不是就可以省略 Refresh Token 了,另外如果攻击者获取了 Refresh Token,那么他也可以一直申请有效的 Access Token,Refresh Token 解决了什么问题呢?

Refresh Token 是否多余?

不多余,Refresh Token 降低了攻击面。

例如 Access Token 的有效时间是 15 分钟,一个小时内,请求 80 次接口,那么 Access Token 的暴露风险就是 80 分,而一小时内调用 4 次 Refresh Token,受到攻击的风险是 4 分,Refresh Token 请求的频率很低,所以被攻击的风险更低。

攻击者即使截获了 Access Token,在短有效期的前提下,影响也是有限的。

更详细的解释参考:Stackoverflow Answer

Opaque Access Token 与 JWT Token

根据 OAuth 2.0 的设计,Access Token 可以分为两种 —— Opaque Access Token 与 JWT Token

Opaque Access Token 是一个随机生成的字符串,服务端会将该字符串与对应的用户身份信息存储在服务器端,客户端只需要将该 Token 在请求中携带即可,不需要对 Token 进行解析,该方式相对简单,但服务端需要存储 Token 对应的身份信息,因此在集群部署时需要进行一定的同步或共享。

JSON Web Token(JWT)是一种开放标准(RFC 7519),是在 OAuth 2.0 的基础上发展而来的一种身份验证方案,最初由 Auth0 公司于 2015 年推出,它将访问令牌的信息进行了 JSON 格式的编码,同时包含了数字签名,可以确保信息的完整性和安全性。

JWT Token 的优劣势

Opaque Access Token 有点儿像是传统的 Session 会话方式,Access Token 跟 User 绑定,通过查缓存、查数据库的方式获取用户的认证信息和权限,这种方式感觉上 SessionId 有相似之处。主要区别在于 SessionId 是用于维护 Web 应用程序的用户状态,而 Opaque Access Token 是用于验证 API 请求的合法性。

JWT 相较于 Opaque Access Token 的优缺点如下,反过来就是 Opaque Access Token 的优缺点。

优点

  • 服务端可以通过 Token 内部的信息进行验证,而不需要再次查询数据库或存储 Token 对应的身份信息,服务端不存储 Token,更节省资源,速度也更快。
  • 通过 JWT 可以跟客户端交换 JSON 数据,具有更高的可扩展性。

劣势

  • 相比 Opaque Access Token,JWT 的长度较长,需要占用更多的网络带宽和存储空间,不适合大规模系统。
  • 无法通过简单的方式立即注销 Access Token,只能等 Access Token 过有效期后自己失效。
  • 如果 JWT 信息未经加密,可能会泄露一些用户信息,安全性要求高的场景,JWT 携带的数据应加密后发送。

实践

除了典型的认证和授权场景,在实际的应用中 Token 可能会指代更广泛的用途,例如服务和服务间通信,Token 可能来自于平台的申请,此时 Token 就不会过期,丢失或泄露后需手动重置。

在另一些场景,服务端也可能提供给用户不失效的 Token,其伴随其账号的整个生命周期,不过一般这样处理都是为了方便、且用在对安全不敏感的场景。

总结

软件开发没有银弹,任何方案都有其优势和不足,只有结合业务场景,做好取舍,才能找到最适合的技术方案。

参考