客户端与服务端的交互凭证模式

267 阅读8分钟

一、凭证的出现

互联网诞生之初,它的作用基本上是静态的信息展示,用户在使用互联网时的大多数操作就是浏览网页,在这种情况下互联网应用的服务器不需要感知用户的操作状态,HTTP的无状态通信机制,只要将数据从服务端传输到浏览器供用户浏览就算完成任务了。但是后来的互联网产品逐渐丰富起来,人们通过互联网进行网购、发帖等操作,此时的服务器必须要区分浏览器前的每一个用户,记录他们的信息和状态,并在交互操作中传递这些信息。

怎么实现这个需求呢?问题的本质是中心化的服务器需要先识别出不同的终端用户,然后再对用户行为进行响应。拆解下来,服务器需要做的事情是:

(1)询问:你是谁?

(2)识别:你能干什么?

(3)判断:如何证明?

我们可以把询问“你是谁”的这个逻辑称为用户认证,把识别“你能干什么”的过程定义为授权,授权之后需要颁发一个用户凭证,用户拿着这个凭证再进行后续操作,服务器就可以以此为判断,对不同用户的行为加以区分和处理了。

二、Cookie-Session模式

互联网电商时代最常见的凭证模式当属Cookie-Session,我们常把cookie和session放在一起说,刚接触时难免会混为一谈。但他们是不同的,他们分别存储在客户端和服务端共同完成整体的交互协作。

  1. 关于cookie

RFC6265规定了HTTP的状态管理机制:在 HTTP 协议中增加了 Set-Cookie 指令,该指令的含义是以键值对的方式向客户端发送一组信息,此信息将在此后一段时间内的每次 HTTP 请求中,以名为 Cookie 的 Header 附带着重新发回给服务端,以便服务端区分来自不同客户端的请求。

由此我们可知:cookie是保存在客户端的数据,由服务端产生并传给客户端,客户端以Key-value的形式保存在本地,后续再向此服务器发送其他请求时,都会带上这个cookie,以便服务器区分请求来自哪个客户端。

  1. 关于Session

Session即为会话,服务器与客户端进行通信时,需要识别出是哪个客户端,并把这个客户端的信息(比如用户id,角色,登录时间等)加载到对话的上下文中,服务器会为每一个用户创建一个单独的标识(比如叫sessionId),每次客户端向服务端发起请求时携带这个sessionId,服务器就能快速判断出这是哪个客户端的请求,并且查询到此sessionId关联的其他用户信息。简单理解session就是存储在服务器中的用户状态信息。

  1. Cookie-session模式运作过程

如下图所示

1.png

  1. Cookie-session模式的局限性

分布式系统有一个不可能三角:一致性(Consistency)、可用性(Availability)和分区容忍度(Partition Tolerance)不可兼得。Cookie-session模式在CAP面前也具有局限性:

首先,在一致性方面:在分布式多节点服务集群中,客户端的请求有可能随机分散在不同的服务器节点上,这样一来,不同服务器节点上保存的session信息很容易出现不一致的情况。

其次,可用性方面:如果要解决一致性的问题,比如根据客户端IP或者用户ID分配不同用户请求到指定的服务器上,不同服务器上不会存在同一份用户信息,这样解决了一致性问题,但是万一某一个服务器节点宕机,则会造成部分用户数据的丢失,造成分布式系统的服务可用性下降。

最后,分区容忍度方面:session数据不保存在普通服务器节点上,而是采用一个特殊的服务器(比如redis)存储所有的session数据,并使普通服务器都可以访问它。这种做法下的矛盾点就在于数据单点存储在这个特殊服务器上,成为单点瓶颈,一旦它不可访问,就出现了分区容忍度问题。

目前,业界的普遍做法是采用上述第三种方式,用高可用的redis集群来存储session数据,能够在很大程度上满足CAP三角。

三、JWT模式

我们已经知道了采用Cookie-session,即把用户信息存放在服务端这种模式,存在分布式系统的不可能三角问题,那么我们可以思考换一种思路——把用户信息存放在客户端,延续这种思路,我们可以发现有JWT模式。这种方式的好处是把用户状态信息放在客户端,每次请求携带着这些状态信息发给服务端,服务端就不用考虑存储用户的状态信息了。

  1. JWT是什么

JWT是JSON Web Tokens的缩写,由dot(.)分隔的三个部分组成,它们是:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

下图为来自JWT官网(jwt.io/)的截图:

2.png

其中,第三部分签名算法为:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)

JWT的整体表达式:

JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature

在JWT模式中,服务端如果是单体应用,可以采用JWT的默认签名算法 HMAC SHA256进行签名,因为加密与验证均在一个进程中,不存在秘钥泄漏风险。但是在多方系统中,需要采用非对称加密算法进行签名,把授权服务器独立出来,用于签名的私钥仅保存在授权服务器中,其他服务器仅能获得一个公开的公钥,公钥不能用来签名,但是可以用来验证签名是否由授权服务器所持有的私钥签发。这样其他服务器能够独立判断JWT令牌中信息的真伪,不需要每次都请求授权服务器进行鉴权,同时也能提高系统的安全性。

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务端通信,都要带上这个JWT。你可以把它放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在HTTP请求的头信息 Authorization 字段里面。

Authorization: Bearer <token>
  1. JWT的工作过程

简单示意如下图所示

3.png

  1. JWT的不足之处
  • 令牌难以主动失效:一旦把令牌颁发出去,令牌就掌握在客户端,在到期之前会始终有效,服务端需要增加额外的逻辑去主动让令牌失效。
  • 令牌数据老旧:如果用户信息有更新,很难控制客户端对存储的信息进行更新。
  • 令牌大小受限:HTTP 协议并没有强制约束 Header 的最大长度,但是,各种服务器、浏览器都会有自己的约束,譬如 Tomcat 就要求 Header 最大不超过 8KB,而在 Nginx 中则默认为 4KB,因此当用户状态信息较多时,不能采用这种模式。
  • 网络开销加大:状态信息存储在客户端,每次请求携带这些数据,会加大网络开销。
  • 客户端需要考虑如何存储JWT,是否需要持久化存储,以及如何安全的存储。
  • 服务器难以拓展服务:服务端不保存用户状态,就缺少了一些用户数据,在实现某些功能时会变得难以实现。比如,做一个在线用户实时统计功能。

四、小结

伴随客户端与服务端的交互需求的出现,服务端需要在每次通信时感知客户端是谁,能做什么,以及如何证明。相应而生了很多种方式方法,本文介绍的是目前主流的两种方式,无论是cookie-session还是JWT,它们本身都有一些局限性,都有自己适合的场景,抛开场景谈优劣都是不适合的。比如在设计一个大型复杂分布式系统时,把JWT作为一次性令牌使用,分布式系统继续使用Cookie-Session做会话管理,但可以在认证鉴权后生成JWT做分布式系统内部服务调用间的一次性令牌,这样就能够很好的取长补短。实践时,首先了解每一种模式的工作原理和特点,再结合具体的需求场景挑选或者制定适合的授权-凭证验证方案,才是最优的实践方案。