前端之搞懂SSO、CAS、JWT

868 阅读9分钟

本篇文章分享来自小伙伴「huangzecheng」的一次学习总结分享,希望跟社区的同学一起探讨。

一、鉴权

自万维网诞生以来,鉴权一直是软件开发工作者们绕不开的一个话题,只要是涉及到用户的应用服务,必然需要对用户身份进行校验甚至区分权限来进行业务控制,在不断的实践过程中,鉴权也不断形成了标准,就如下面这个经典的用户认证过程: 鉴权.png 这种鉴权方式的好处是,减少了客户端和服务端之间的通信数据量,避免了如身份信息等敏感信息在信道中的传输,提高了安全性,也方便了前后端人员,减少了因敏感信息传送而产生的规约。

SSO

通过上面的案例过程,我们已经理解了如何在客户端和服务端之间拉起认证过程,但是这种场景也有弊端,当下的应用,许多都是以应用群的形式存在,如第一梯队的阿里系旗下的各类子站如淘宝、天猫、一淘,如果说每个网站都需要重新输入账号密码登录一遍,不仅降低了体验,不利于用户粘性,也增加了敏感信息的传输频次,不利于安全性。因此我们需要一种在一处登录,多点可共享登录状态的模式,即 SSO(Single Sign On) 单点登录。 这种模式和当下健康码的演进有点类似,健康码应用的初期,各类健康码、地方码、行程码五花八门,种类繁多,不同地区,不同场所的要求也不一样,每次要打开好几个应用示码才能放行,常常流转于各个场所间会非常麻烦。 image.png 有了这样的痛点,自然会有人解决,在各平台的不断推动下,逐渐推出了类似“三码合一”的应用服务,即打开一次应用就可以展示不同场所需要的认证信息,不必再一个个打开应用,大大解决了时间和操作成本。 下面是一个 SSO 服务的简单图解:

SSO.jpg

综合来讲,SSO 是提升用户体验以及安全性的一个非常行之有效的办法。 更详细的讲,我们要把 SSO 分为以下几种情况:

同域 SSO

根据上面的模型,我们可以发现,我们需要一个公用的存放用户凭证的点以便在不同应用服务需要鉴权的时候递送过去,通过浏览器的同域特性,我们得以用 cookie 来实现这个场景,即: 同域SSO.jpg 由于A和B的前端能共享 cookie 且后端的认证系统也互通,因此无需额外的服务即可实现 SSO。 格外注意的是,同域范畴也包括了同父域,只需要将域名设置为同一个父域名即可。

跨域 SSO

参考了同域 SSO 的案例,可以得出,跨域 SSO 无法通过前端共享存储的方式来实现,此时的A和B在安全机制下是隔离无法获取对方的任何信息的。因此我们需要一个或多个额外的服务来支持我们实现 SSO,其中就包括了 CAS 这样的解决方案。

CAS

CAS(Central Authentication Service) 是 Yale 大学发起的构建 Web SSO 的 开源项目,他产生的比较早主要是PC时代,设计的也很好,只是在app时代没有那么强的包容性。 CAS分为1.0和2.0版本。1.0 是基础模式,2.0 为代理模式,适用与非 web 应用的 SSO,1.0 适用于 web 应用。 首先我们讲一下 CAS1.0 的实现:

术语 Client:用户 Server:中心服务器,即 SSO 中负责登录的 service Service:需要 SSO 的各个服务,对应的是不同的业务 service 票据 TGT:Ticket Granting Ticket TGT 是 CAS 为用户签发的登录 ticket,也是用于验证用户登录成功的唯一方式。 TGT 封装了 Cookie 值以及 Cookie 值对应的用户信息,CAS 通过 Cookie 值(TGC)为 key 查询缓存中有无 TGT(TGC:TGT(key:value)),如果有的话就说明用户已经登录成。

TGC:Ticket Granting Cookie CAS 会将生成的 TGT 放在 session 中,而 TGC 就是这个 session 的唯一标识(sessionId),可以认为是 TGT 的key,为 TGT 就是 TGC 的 value,TGC 以 cookie 的形式保存在浏览器中,每个请求都会尝试携带 TGC。(每个服务都会在 session 和 cookie 中保存对应的 TGT 和 TGC)

ST:Service Ticket ST 是当用户访问某一服务时提供的 ticket。用户在访问其他服务时,发现没有 cookie 或 ST ,那么就会302到 CAS 服务器获取 ST。然后会携带着 ST 302 回来。

详细过程 下面用一个详细的例子来描述CAS的过程:

  1. 用户访问应用 A,域名是 www.a.cn。
  2. 由于用户没有携带在 A 服务器上登录的 A cookie,所以 A 服务器返回 http 重定向,重定向的 url 是 SSO 服务器的地址,同时 url 的 query 中通过参数指明登录成功后,回跳到 A 页面。重定向的url 形如 sso.cas.cn/login?service=https%3A%2F%2Fwww.a.cn
  3. 由于用户没有携带在 SSO 服务器上登录的 TGC(看上面,票据之一),所以 SSO 服务器判断用户未登录,给用户显示统一登录界面。用户在 SSO 的页面上进行登录操作。
  4. 登录成功后,SSO 服务器构建用户在 SSO 登录的 TGT(又一个票据),同时返回一个 http 重定向。这里注意:
    • 重定向地址为之前写在 query 里的 A 页面。
    • 重定向地址的 query 中包含 SSO 服务器派发的 ST。
    • 重定向的 http response 中包含写 cookie 的 header。这个 cookie 代表用户在 SSO 中的登录状态,它的值就是 TGC。
  5. 浏览器重定向到产品 A。此时重定向的 url 中携带着 SSO 服务器生成的 ST。
  6. 根据 ST,A 服务器向 SSO 服务器发送请求,SSO 服务器验证票据的有效性。验证成功后,A 服务器知道用户已经在 SSO 登录了,于是 A 服务器构建用户登录 session,记为 A session。并将 cookie 写入浏览器。注意,此处的 cookie 和 session 保存的是用户在 a 服务器的登录状态,和 CAS 无关。
  7. 之后用户访问产品 B,域名是 www.b.cn。 
  8. 由于用户没有携带在 B 服务器上登录的 B cookie,所以 B 服务器返回 http 重定向,重定向的 url 是 SSO 服务器的地址,去询问用户在 SSO 中的登录状态。
  9. 浏览器重定向到 SSO。注意,第 4 步中已经向浏览器写入了携带 TGC 的cookie,所以此时 SSO 服务器可以拿到,根据 TGC 去查找 TGT,如果找到,就判断用户已经在 SSO 登录过了。
  10. SSO 服务器返回一个重定向,重定向携带 ST。注意,这里的 ST 和第4步中的 ST 是不一样的,事实上,每次生成的 ST 都是不一样的。
  11. 浏览器带 ST 重定向到 B 服务器,和第 5 步一样。
  12. B 服务器根据票据向 SSO 服务器发送请求,票据验证通过后,B 服务器知道用户已经在 SSO 登录了,于是生成 B session,向浏览器写入 B cookie。

步骤比较多,理解起来可能稍费力,总结来讲的话,其实就是搭建一个独立的 SSO 服务,在A或B服务未鉴权或鉴权失败的情况下跳转到 SSO 服务来进行鉴权,并返还鉴权凭证给A或B服务,来完成服务间的认证共享(A或B登录其一即可,因为他们的登录凭证都由 SSO 来发放)。

JWT

JWT(JSON Web Token) 是目前最流行的跨域认证解决方案,它也可以 实现类似 CAS 的功能,但也存在许多不同。最显著的差异是,JWT 并不保存用户数据,它会将所有数据都保存在客户端,并在每次请求时发回服务器,下面将详细介绍下 JWT 的原理。

JWT的结构

前面讲到 JWT 在登录认证后将返还的用户信息以 JSON 的形式保存在了客户端,并在请求时发送给服务端,其保存的形式如下

{
  "user_name": "皮皮虾",
  "user_id": "ppx123456",
  "password": "123456"
}

实际的 JWT 则由以下三部分组成

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

上面所说的JSON就是Payload(负载), 三者结合起来拼成了一个 JWT 令牌:

Header.Payload.Signature
eyJhbGciOiJIUzI1NiJ9.eyJleHBpcmVUaW1lIjoxNTg5MDE0MzA2OTU0LCJkZXB0Ijoi5Yas6KW_55Oc5ZywIiwidXNlcm5hbWUiOiLlpKfopb_nk5wifQ.QSv0FcvNheiA3FW6OEah7jJKG4SG0ver3q67F0980rY

这里再补充下 Header 和 Signature 的说明 Header Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。

Signature Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

JWT的使用

和 CAS 类似的,客户端收到认证返回的 JWT 后,需要将令牌存储在 cookie 中,也可以存储在 localStorage中,在通信请求时,可以将其放在 HTTP 请求的头信息中,或者放在 POST 请求的 Body 中。

JWT优劣

  1. 安全性

由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。

  1. 性能

JWT 太长。由于是无状态使用 JWT,所有的数据都被放到 JWT 里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致 JWT 非常长,cookie 的限制大小一般是4k,cookie 很可能放不下,所以jwt一般放在 localStorage 里面。并且用户在系统中的每一次 http 请求都会把 JWT 携带在 Header 里面,http 请求的Header可能比 Body 还要大。而 sessionId 只是很短的一个字符串,因此使用 JWT 的 http 请求比使用 session 的开销大得多。

  1. 一次性

无状态是 JWT 的特点,但也导致了这个问题,JWT 是一次性的。想修改里面的内容,就必须签发一个新的 JWT。