About Session, JWT and OAuth2

最近把项目部署到云函数上时,一直出现跨域问题——登陆能登上,能拿到和设置 cookie,请求的时候也能带上 cookie,可服务端就是拿不到 Cookie(已经使用了 SpringSessionJdbc 以在不同云函数实例下共享 Session);研究发现这似乎是 Cookie 的 SameSite 属性导致的…气急败坏之下,考虑使用无状态,不依赖 Cookie 的 JWT 来进行权限认证以避免这种劳什子问题。

于是这里对 JWT 进行一些学习,以及使用其进行 web 应用权限认证的最佳实践。之后将放弃 Shiro(Shiro 似乎只支持 Cookie 的方式),使用 Spring Security 来实现权限认证。这里也顺便学习一下 OAuth2——OAuth2 通常会使用 JWT 或类似的技术去进行校验,之后项目也可以考虑去接入 Github 或其它 OAuth2 授权服务,方便用户登陆。

Shiro 对 JWT 支持不好,Spring Security 过于复杂(且这种复杂度是过度的…实现最简单的满足业务需要的认证功能都需要编写一堆代码,接触一堆概念,这我还不如自己造轮子!),最终我还是选择一个国产的框架 Sa-Token 来实现权限功能……

其实使用这个框架的话应当能配置 Cookie,但该框架未提供和 session-jdbc 的集成,因此不考虑 Cookie 方案。


Session vs JWT

JWT,即 JSON Web Tokens,本质上仍是一种 Token,因此先研究 Token 是必要的,而 Token 很适合同 Session 进行对比。

Session 是比较熟悉的,用户登陆时,服务端会保存用户的 Session 信息到内存中,并在 Cookie 中置一个 sessionId 作为用户的 Session 信息的 key;用户请求时,服务端再检查 sessionId,找到用户的权限信息。

Session 会有导致诸多问题:

  1. 跨域问题,这不必多说,解决方案是对 Cookie 进行配置,设置 CORS 等。
  2. 分布式下 Session 共享问题,假设服务端是分布式的,则 Session 信息可能仅保存在某个服务器中,而打到其它服务器上的请求就拿不到 Session 信息了。解决方案是将 Session 集中保存在某台服务器或数据库或 redis,但这仍旧会影响服务的横向扩展
  3. 单点登陆问题,这和第 2 点问题应当比较类似,解决方案也类似,但缺点仍类似。

Session 的问题归根结底就是因为,Session 是有状态的,服务端需要维护 Session 信息

似乎也有客户端的 session 方案,就是让客户端持有服务端校验所需的信息,这样服务端就不需要维护 Session 状态了,只需要每次校验时比对数据库即可……但这和 token 其实很类似了。

而 Token,Token 的服务端来说是无状态的用户的权限信息直接保存在客户端上,且服务端能在本地(不访问数据库)对用户的权限信息进行校验。容易发现,Token 天生就是分布式的。

JWT 就是一种 Token。使用 JWT 时,在用户登陆时,用户向认证服务器发送账号密码信息后能拿到 Token,其中包括权限信息以及这些信息使用认证服务器的私钥进行的签名;用户向服务端发送请求时,服务端使用公钥解密签名并对权限信息进行验证。

Token 有诸多好处:

  1. 对数据库无压力,服务端进行权限信息的认证,获取用户权限时不需要访问数据库,在本地即可完成,且由于权限信息带上了签名,也能防止伪造权限。
  2. 认证和校验分离,认证服务器可以独立出来甚至让相应服务商负责,服务端只需拿到公钥即可,且由于该公钥只用于验签,被恶意者拿到公钥也不会有任何影响。
  3. 分布式友好,每台持有公钥的服务器都能独立进行权限校验,不需要依赖第三方服务,因此横向扩展没有任何影响。
  4. 跨平台,不是所有平台都能使用 Cookie,而使用 Token 则简单许多
  5. 无痛实现单点登录

同样的,Token 也会有一些缺点:

  1. 无法直接实现登出功能,因为权限信息在客户端而非服务端,但是有一些解决方案。
  2. JWT 的 Token 的信息是可以直接读出来的,无法(也不应该)存储敏感数据
  3. 权限更新会有延迟——需在旧的 token 过期时才能使用新的权限

这些问题都需要相应的解决方式。

OAuth2

一言以蔽之,OAuth2 是一种授权协议/框架,它允许网页或应用代表用户去访问保存在其它应用上的资源。

OAuth2 中有 4 个角色,其中的 Resource,资源可以认为指用户信息:

  • Resource Owner,资源持有者,可以认为是用户本身——用户是资源的拥有者;用户授权应用去访问他们的账户。
  • Client,很让人混淆,但这里指的是要访问用户信息的应用,应用需要在用户的授权下才能去访问用户信息,这个授权会被(资源服务器的?)API 所校验
  • Resource Server,资源服务器,持有资源即用户信息的服务器
  • Authorization Server,授权服务器,验证用户身份并向应用返回令牌,应用使用此令牌访问 Resource Server

从应用开发者的视角来看,很多时候一个服务同时担任资源服务器和授权服务器的角色,这时候称其为 Service(服务),或 API 角色。但也不排除 Client 和 Service 为同一个角色的情况。

上图比较清晰地说明了 OAuth2 验证流程:

  1. 应用向用户去请求访问服务资源的权限(比如跳转到 github OAuth2 请求登陆的界面,要求用户输 github 账号密码,确认授权啥的?)
  2. 若用户授权,则应用能拿到一个授权(authorization grant)
  3. 则应用使用这个授权和应用自己的鉴权信息向授权服务器请求,拿到令牌
  4. 若授权和应用的鉴权信息均合法,则应用拿到令牌,授权完成
  5. 应用带上令牌作为鉴权信息向资源服务器发送请求
  6. 若令牌合法,则资源服务器向应用返回资源

authorization grant 的类型不同会决定具体的流程。

比如我想让我的应用能够用 Github 账号去登陆,此时 Resource Owner 是使用 Github 账号登陆我的应用的用户,Client 是我的应用,Resource Server 和 Authorization Server 是 Github 暴露出来的 API。用户授权后,我从 Github 拿到用户的相关信息如用户名,邮箱等,并操作自己的数据库,完成注册,登陆等操作。

这里只提大体的框架,深入学习待应用的时候再说。


参考资料


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!