cas单点登陆设计与实现

背景及问题引出

一些大企业如阿里巴巴, 对它旗下的各种网站都做了单点登陆的设计。当进入其中一个网站点击登陆时, 浏览器会被重定向到一个登陆的平台进行登陆。在登陆完成后, 我们又能跳回到原来的页面, 并且此时状态更改为“已登陆”。之后访问阿里巴巴的其它相关网站时, 也会保留登陆状态

我们可能想到让同一顶级域名的各个网站使用统一的cookie来实现这个效果。比如我们将cookiedomain项设置为aliyun.com, 那么www.aliyun.comdeveloper.aliyun.com就能共享cookie, 借此来维持不同网站的统一登陆状态

上面的方案是可行的, 但是限制很明显, 那就是当顶级域名不同时, cookie是没法共享的。那么alibaba.com域名下的网站和aliyun.com域名下的网站就没法共享登陆状态了

不同顶级域名的服务必然没法共享cookie, 这是浏览器的安全限制。想想cookie在不同域名上是共享的, 那一切就乱套了, 会使安全性大幅降低, 任何一个网站都能拿取session, 然后用它绕过登陆环节, 轻而易举地拿到用户数据

现在的目标就是, 如果让跨顶级域名的不同网站, 共享登陆状态, 下面来提出解决方案

解决方案: Central Authentication Service(CAS)

我们将登陆系统做成一个单独的服务, 我们称之为网站O(https://www.o.com), 对于其它分立的服务, 我们称之为网站A(https://www.a.com), 网站B(https://www.b.com)。下面是常见的cas登陆流程

1.用户进入网站A, 此时网站A检查cookie, 查询session信息, 发现没有用户session, 就将用户重定向至https://www.o.com/login进行登陆, 并带有以下的query params

1
2
website_code=一串代表网站A身份的字符串, 它之前就被存在了网站O的数据库中了
login_callback=https://www.a.com/callback/login

2.网站O收到信息, 检查cookie, 查询session信息, 发现没有用户session(这里请注意, 网站A和网站O都维护了自己的一套session, 它们不是同一个东西), 那么将登陆界面渲染给用户, 然后用户在浏览器执行一系列登陆操作

3.当用户点击登陆后, 网站O将建立session, 并设置用户cookie, 来保持网站O的登陆状态。接下来网站O将重定向到https//www.a.com/callback/login(也就是第一步填写的回调接口), 并带有一个ticket放在query params里面

4.https://www.a.com/callback/login的回调请求中, 网站A将访问O网站的ticket校验接口, 将得到用户的相关信息

5.接下来网站A设置网站A自己私有的cookie, 来维持网站A的登陆状态

那么B网站如何共享这个登陆状态? 见下面的步骤

1.用户进入网站B, 此时网站B检查cookie, 发现没有, 就将用户重定向至https://www.o.com/login, 并带有以下的query params

1
login_callback=https://www.b.com/callback/login

2.网站O受到信息, 检查cookie, 发现已经有合法的cookie了, 那么直接重定向到https://www.b.com/callback/login, 并附有ticket在query params

3.网站A通过ticket访问网站O, 拿到用户信息, 并设置cookie维持网站A的登陆状态

那么如何统一下线? 见下面的步骤

1.用户现在在网站B, 点击注销按钮, 跳转到网站B的注销接口https://www.a.com/logout, 网站B清除cookie信息, 然后将用户重定向到https://www.o.com/logout

2.网站O通过cookie, 注销网站O维持的登陆状态(session), 并给所有网站的下线接口发送通知, 它们会删除相关的session信息, 进行下线

3.当用户再次访问网站A时, 网站A拿到的cookie没法定位到一个session登陆信息, 那么就在未登陆的状态了, 此时网站A重定向到https//www.a.com/callback/login进行登陆…(又回到了原点)

设计细节

上面省略了许多应当考虑的点, 这里依次补充

1.如何防止对网站O非法的请求? 比如用户直接访问https://www.o.com/login?login_callback=https://pronhub.com。这样岂不是相当于我们的服务器要访问pornhub, 然后被查水表?

解决方案: 从网站A, 重定向到O网站的时候, 应当带上网站A的身份信息(比如一串独一无二的字符串, 可以存放在query params中, 这些信息是之前就存放在网站O的数据库之中的), 然后网站O通过身份信息查询数据库, 拿到网站A的IP, 需要检验两个回调地址是否属于网站A的IP。这样能够防止重定向到非法的地方

2.网站A和网站B的session, 时间应该为多长?

无限长, 直到此网站的下线接口被调用, 删除相关session

3.网站O的session, 应该为多长?

网站O的session应该保持一定时限, 看具体业务而定

4.网站O的session如何管理?

应该保存在数据库中, 且在删除session的时候必须执行回调操作, 如果删除了网站O的session却没有调用下线回调的话, 那么子平台就不能下线!网站O可以随时删除session, 这样的安全性保证很高, 且用户修改密码后, 用户也能正常的在所有子平台下线

5.如果下线回调失败?

应该考虑, 子系统与登陆系统之间的网络情况。可以在失败时尝试一定次数, 回调应该交给守护进程/线程/协程执行, 如果一直失败, 打印日志排查问题

6.网站O应当保存的信息有哪些?

session以及子系统的站点信息, 前者不多说, 子站点的信息我认为至少下面的数据

  • 子系统的身份代码(在重定向到https://www.o.com/login时, 要指定子系统的身份代码, 见前面的例子, 有个website_code, 就代表这个身份代码)
  • 子系统的IP(因为需要验证那个回调地址是否属于子系统A, 否则就可能会跳转到pornhub上去了, 见前面的例子)
  • 子系统的下线回调地址(进行统一下线)

7.承接上个问题, 为什么不把下线回调地址也做到参数里面, 这样不是自由度更高?

是的, 可以那么做, 最后检验下下线回调的地址是否为IP也同样安全, 但是处于简便性考虑, 本文假定每个子系统只有唯一的一个下线回调

8.关于上面提到的注销例子, 是否应该考虑不给网站B发送注销回调以减小开销? 比如在注销接口上带上该网站的身份代码, 告诉服务器O别向此网站发送注销回调?

如果这么设计, 有一种可能, 用户直接访问https://www.o.com/logout&xxx=网站B的身份代码(虽然这属于用户找茬, 但仍然值得考虑), 那么就会发生用户除了在网站B保持登陆, 在其他网站都是未登陆状态的问题。这显然很不合理, 因此需要给所有网站发送回调(即使可能让子系统删除不存在的session, 但是这不是什么问题)

代码实现:

我将在近期开源到github上面