跨域单点登录方案实现

SSO英文全称Single Sign On,单点登录。当我们搜索单点登录的时候,会发现很多的文章,然而这些文章一般都是基于一种通用的场景描述,通常在各自的业务环境会更加复杂。在本篇文章,我将描述具体场景下实现单点登录的方案。

SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。

背景

企业发展初期,一般一个域名站点便可承载独立业务。但随企业对于新业务的探索,便都会申请一个新的域名用于这部分新业务的功能承载,一方面是为了做区分,另一方面是满足监管的要求。并且需要在这种模式下打通原先站点的用户体系。新的独立域名主要有两种:

  1. 原有站点是www.a.com,新独立域名是new.a.com;
  2. 原有站点是www.a.com,新独立域名是www.b.com;

对于第一种场景,一般我们采用共享session的方式就可以做到用户在不同域名之间跳转而无需重复登录。具体实现主要是将cookies中关于用户登录态的sessionid的domain设置为.a.com

这种场景较为简单,实现上做好新老模式之间的切换即可。因为默认情况下sessionId的domian是www.a.com,如果之前已经访问过www.a.com站点,且登录的时候未清除掉domian为www.a.com的sessionid,那么访问www.a.com的站点,浏览器会把两个同名sessionId传递到服务端,由于是key-value的形式,服务端无法分辨哪个是新的,哪个是旧的,如果取了旧的,那么就无法获取用户此时登录状态。解决这个问题,只需要在设置sessionId的时候把原有domian为www.a.com的置为过期,或者用一个新的sessionId键即可。具体查看set-cookies介绍

这种场景在我们的移动站点用的比较多。例如主站www.a.com,移动站点为m.a.com这种场景。

#设置domian为.a.com的sessionId
Set-Cookie: sessionId=a3fWa; Domain=.a.com; Secure; HttpOnly

#将domian为www.a.com的sessionId置为过期
Set-Cookie: sessionId=a3fWa; Domain=www.a.com; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly
复制代码

接下来我们主要描述第二种场景。对于这种场景,主要要求是用户无感知,需要做到以下几点:

  1. a站点登录之后往b站点同步登录态;
  2. 访问b站点无需登录页面后,需要主动同步a站点登录态;
  3. 访问b站点需要登录页面时,需要跳转到a站点做登录态同步;

流程一:a站点登录之后往b站点同步登录态

登录之后同步登录态时序图

以上是主动同步登录态的时序图。图示中的ticket主要存放在redis中,你也可以存放在其他的存储媒介甚至应用运行内存,但是需要注意的一点就是ticket应一次有效,用过之后需要清除掉。由于这里我们的b站点也在自己的受控范围,并且redis的读写性能也相当优越,所以a和b连接并读取了同个redis。如果b站点不在受控范围内,可在b站点后台发起一个请求到a站点询问ticket的有效状态。具体流程如下:

  1. 用户访问a.com的登录页;
  2. 输入用户名密码登录,a.com后台校验用户,成功之后生成a站点的sesion并生成一个ticket放入redis中;
  3. 登录页面登录成功之后,拿到ticket往b.com发送一个跨域请求(JSONP或者Image);
  4. b站点获取到ticket之后,检验在redis是否存在,存在着设置b站点session并删除ticket;
  5. 跨域请求返回之后继续其他操作,如跳转用户中心,首页等。

流程二:b站点无需登录页面主动同步a站点登录态

b站点无需登录页面主动同步a站点登录态时序图

由于a、b站点相互独立,假设各自的session过期时间为半小时,如果a站点一直处于访问状态,那么session会一直续命下去,但是b站点由于超过30分钟没有访问,session状态已经过期,这时候访问b站点就会有这个场景了。具体流程如下:

  1. 用户访问b.com无需登录页面;
  2. 如果当前站点用户未登录,发起异步JSONP请求到a.com;
  3. 如果a.com未登录,不做任何操作。如果已经登录,跟上个流程一样,生成ticket信息;
  4. 拿到ticket之后,请求b站点同步登录状态,b站点生成session;
  5. 同步成功之后主动重新刷新当前页面。

流程三:b站点需登录页面主动跳转到a站点做登录态同步;

b站点需登录页面主动跳转到a站点做登录态同时序图

同流程二,这个场景的出现也是因为长时间未访问登录b站点导致,与流程二不同的是,这个场景是302直接跳转同步页面的方式,由后台直接判断,适合后台直出页面,如果是纯静态页面请使用流程二,具体流程如下:

  1. 用户访问b.com需要登录页面,返回302跳转到a站点的状态同步页面;
  2. a站点状态同步页面判断站点登录状态,未登录状态跳转到a站点的登录页面,登录流程同流程一,登录成功之后跳转b站点需登录页面;
  3. 已经登录状态做JSONP登录状态同步到b站点,b站点生成session;
  4. 同步b站点成功之后跳转到b.com站点需要登录页面。

总结

以上流程主要在于实现SSO过程的针对各种场景的解决方案,根据通过发起方的不同又可分为两类,主动同步和被动同步。主动同步是向认证站点获取ticket并同步自身登录态,被动同步是由认证站点向当前站点同步登录态。

一般网上的资料会有一个专门用作认证登录的站点,比如a.com和b.com站点都从sso.a.com获取认证状态。其实在本文中就是把a站点用作sso站点了,原理上是一致的。

为了这一套方案落地的时候,业务开发同事无需关注这部分实现细节并编写相应的同步代码,我们把他写入了整体框架里面,主要做了一下两件事:

  1. 封装前端登录脚本(支持npm引用,直接webpack打包),登录之后去做主动站点同步,业务同事在需要登录的场景,直接调用方法并在回调函数做其他操作;
  2. 使用ejs或者pug等模板引擎,将流程二的同步过程放入通用模板,其他页面在通用模板基础上创建。

更多关于sso认证方面的可以参考OAuth2的流程,这套流程用于不可信站点之间的认证在安全方面会更加成熟些,也是目前微信采用的认证流程。

FYI:

单点登录(SSO)看这一篇就够了
SSO单点登录三种情况的实现方式详解
微信网页授权