0%

基于前后端分离的CAS对接方案

最近开发的项目,需要对接公司已有的 CAS 服务,之前的项目都是用的传统模式(后端渲染或前后端不分离)来开发的,所以后端可以很容易的实现控制跳转,完成校验,写入 Cookies 等逻辑。在传统的开发模式下,使用 session-cookie 可以保证接口安全,在没有登录的情况下访问关键数据会跳转到登录界面或者请求失败。而使用 REStful API 之后,session-cookie 存在以下 3 个问题:

  • 客户端除了浏览器,可能还包括手机端 APP,对于手机端而言,管理 cookie 是一件麻烦的事情
  • RESTful 风格的 API 不建议使用 cookie
  • Cookie 本身有一个缺陷,不能跨域

解决这个问题的方案是让前端传数据时,在 URL 参数中或者 header 中携带一个 参数,我们成这个参数为 token,后端通过这个 token 来判断用户身份,这样可以免去对 Cookies 的管理。

顺着这个思路,我们很容易想到一种方案,就是后端维护一个 Mapkey 值为 tokenvalue 为用户信息,这样只要用户登录时生成这个 key 后,放到 Map 中,然后将 key 返回给前端就行了,但是这样做有个很严重的问题,Map 是在内存中的,如果后端服务为集群时,还需要做 key 同步,非常麻烦,当然也有人会提出可以将后端生成的 token 和对应的用户信息放在 键-值 数据库中,这样就不用考虑同步问题了,当然这样做没有什么问题,但是会额外引入基础组件(我们现在做第一版,为了快速开发,不打算引入太多的组件),而且还要保证键值数据库的高可用性。

这里我使用了一个更加优雅的方案:JSON Web Token

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。

JWT的组成:

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

载荷(Payload):

Payload 是 JWT 存储信息的部分。Payload 也是一个 json 数据,每一个 json 的 key-value 称为一个声明。

我们将用户信息描述成一个 json 对象。其中添加了一些其他的信息,帮助今后收到这个 JWT 的服务器理解这个 JWT 。

1
2
3
4
5
6
7
8
9
{
"iss": "Panmax JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "[email protected]",
"user_id": 1,
"username": "jiapan"
}

这里面的前五个字段都是由JWT的标准所定义的

  • iss: 该JWT的签发者
  • sub: 该JWT所面向的用户
  • aud: 接收该JWT的一方
  • exp(expires): JWT 的过期时间,是一个 unxi 时间戳
  • iat(issued at): JWT 的签发时间,是一个 unix 时间戳

上面这个 payload 中,user_idusername 为自定义声明。

将上面的 json 对象进行base64编码可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。

ewogICAgImlzcyI6ICJQYW5tYXggSldUIiwKICAgICJpYXQiOiAxNDQxNTkzNTAyLAogICAgImV4cCI6IDE0NDE1OTQ3MjIsCiAgICAiYXVkIjogInd3dy5leGFtcGxlLmNvbSIsCiAgICAic3ViIjogImpyb2NrZXRAZXhhbXBsZS5jb20iLAogICAgInVzZXJfaWQiOiAxLAogICAgInVzZXJuYW1lIjogImppYXBhbiIKfQ==

注:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

头部(Header)

WT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

1
2
3
4
{
"typ": "JWT",
"alg": "HS256"
}

在这里,我们说明了这是一个 JWT,并且我们所用的签名算法(后面会提到)是 HS256 算法。

对它也要进行Base64编码,之后的字符串就成了 JWT 的 Header(头部)。

ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9

签名(签名)

将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了

ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ewogICAgImlzcyI6ICJQYW5tYXggSldUIiwKICAgICJpYXQiOiAxNDQxNTkzNTAyLAogICAgImV4cCI6IDE0NDE1OTQ3MjIsCiAgICAiYXVkIjogInd3dy5leGFtcGxlLmNvbSIsCiAgICAic3ViIjogImpyb2NrZXRAZXhhbXBsZS5jb20iLAogICAgInVzZXJfaWQiOiAxLAogICAgInVzZXJuYW1lIjogImppYXBhbiIKfQ==

最后,我们将上面拼接完的字符串用 HS256 算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密之后,得到一串加密字符串,最后把这串加密字符串也是用 . 拼接在 header.payload 后面,形成完整的 JWT。

最终生成的JWT格式如下:

xxx.yyy.zzz

这里签名的目的是为了保证 payload 数据的完整性。如果 JWT 在传输过程中被第三方劫持,中间人对 header.payload 进行修改,并且使用自己的密钥重新签名。服务端收到中间人修改过的 JWT,使用自己的密钥对 header.payload 进行再次加密,由于中间人和服务端使用的是不同的密钥签名,所以服务端再次加密的结果肯定和中间人加密的结果不一致,由此可以断定该 JWT 被恶意篡改。

经过上边的介绍,我们可以看出 JWT 中 Payload 的信息是可以解码会明文的,也就是说信息会泄露,所以 JWT 中不应该存放任何敏感信息,用于登录时我们只需放入用户ID或者用户名就可以了,不要把身份号或者密码等信息放入JWT中,应该让后端拿到用户ID或者用户名后进行查询得到身份证号等隐私信息。

好的,以上就是JWT的科普部分,下边介绍下我这边CAS对接实现方案。

我单独写了一个服务,命名为 hodor(hold the door),作为一个中间层来验证 CAS ticket 并且根据用户信息生成 JWT Token。

我们用 zuul 作为网关服务,当请求过来后,网关判断有没有携带 token,并且判断 token 的有效性,我们需要把网关里验证 jwt 的密钥和 hodor 中生成 jwt 的密钥设置为相同的字符串就可以完成验证工作。

当网关验证 token 没有被篡改并且还在有效期内后,从 Payload 中取出我们需要的信息,将这些信息明文放在 header 中继续往后请求各个应用,对于应用来说从网关过来的请求是可信的,直接从头中取出相应的用户名或者ID就行了。这里有一个坑,就是网关将信息放入 header 的时候,只能传 ASCII 编码字符串,我们的 CAS 返回用户信息时会同时返回中文姓名,所以中文在传入 header 时,需要做 urlencode 处理,同时应用内接受时也需要做 urldecode。

如果没有携带 token 或 token 无效,网关会返回 HTTP 401 错误,前端收到这个返回码后,会跳转到 CAS 认证地址,让用户来登录。同时 service 是前端配置好的一个地址,当用户登录成功后,会回到前端配置好那个地址,前端拿到 CAS 给的 ticket 后,用这个 ticket 和申请 ticket 时的 service 来请求我写的 hodor 服务,hodor根据 ticket 和service 来完成ticket验证工作,获取用户信息,生成jwt,返回给前端,前端保存这个 token,再之后的请求中携带这个 token 来访问就行了。