分三种情况实现单点登录

单点登录是什么

单点登录(Single Sign On),简称 SSO,是目前比较流行的企业业务整合的解决方案之一。

SSO 的定义:在多个应用系统中,用户只需要登陆一次就可以访问所有相互信任的应用系统。

SSO 一般都需要一个独立的认证中心(passport),子系统的登陆均得通过 passport,子系统本身将不参与登陆操作,当一个系统成功登陆以后,passport 将会办法一个令牌给各个子系统,子系统可以拿着令牌获取各自的受保护资源,为了减少频繁认证,各个子系统在被 passport 授权以后,会建立一个局部会话,在一定时间内可以无需再次向 passport 发起认证。

举例
举例

上图有四个系统,分别是 Application1Application2Application3、和 SSO,当 Application1、Application2、Application3 需要登录时,将跳到 SSO 系统,SSO 系统完成登录,其他的应用系统也就随之登录了

举个例子:
淘宝、天猫都属于阿里旗下,当用户登录淘宝后,再打开天猫,系统便自动帮用户登录了天猫,这种现象就属于单点登录

单点登录流程

登陆

登陆流程图
登陆流程图

  • 用户访问系统 1 的受保护资源,系统 1 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
  • sso 认证中心发现用户未登录,将用户引导至登录页面
  • 用户输入用户名密码提交登录申请
  • sso 认证中心校验用户信息,创建用户与 sso 认证中心之间的会话,称为全局会话,同时创建授权令牌
  • sso 认证中心带着令牌跳转会最初的请求地址(系统 1)
  • 系统 1 拿到令牌,去 sso 认证中心校验令牌是否有效
  • sso 认证中心校验令牌,返回有效,注册系统 1
  • 系统 1 使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  • 用户访问系统 2 的受保护资源
  • 系统 2 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
  • sso 认证中心发现用户已登录,跳转回系统 2 的地址,并附上令牌
  • 系统 2 拿到令牌,去 sso 认证中心校验令牌是否有效
  • sso 认证中心校验令牌,返回有效,注册系统 2
  • 系统 2 使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与 sso 认证中心及各个子系统建立会话,用户与 sso 认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过 sso 认证中心,全局会话与局部会话有如下约束关系

  • 局部会话存在,全局会话一定存在
  • 全局会话存在,局部会话不一定存在
  • 全局会话销毁,局部会话必须销毁

注册

注册流程图
注册流程图

sso 认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作。

  • 用户向系统 1 发起注销请求
  • 系统 1 根据用户与系统 1 建立的会话 id 拿到令牌,向 sso 认证中心发起注销请求
  • sso 认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
  • sso 认证中心向所有注册系统发起注销请求
  • 各注册系统接收 sso 认证中心的注销请求,销毁局部会话
  • sso 认证中心引导用户至登录页面

什么是 CAS

什么是 OAuth2

CAS 和 OAuth2 区别

什么是 JWT

如何实现

在将具体实现之前,我们先来聊一聊 Cookie 的作用域

Cookie 的作用域由 domain 属性和 path 属性共同决定。domain 属性的有效值为当前域或其父域的域名 / IP 地址,在 Tomcat 中,domain 属性默认为当前域的域名 / IP 地址。path 属性的有效值是以 “/” 开头的路径,在 Tomcat 中,path 属性默认为当前 Web 应用的上下文路径。

如果将 Cookie 的 domain 属性设置为当前域的父域,那么就认为它是父域 Cookie。Cookie 有一个特点,即父域中的 Cookie 被子域所共享,换言之,子域会自动继承父域中的 Cookie。

利用 Cookie 的这个特点,不难想到,将 Session ID(或 Token)保存到父域中不就行了。没错,我们只需要将 Cookie 的 domain 属性设置为父域的域名(主域名),同时将 Cookie 的 path 属性设置为根路径,这样所有的子域应用就都可以访问到这个 Cookie 了,不过这要求应用系统的域名需建立在一个共同的主域名之下。如 tieba.baidu.com 和 map.baidu.com,它们都建立在 baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录。

总结:此种实现方式比较简单,但不支持跨主域名

不同域名下的单点登录

方法一:认证中心

我们可以部署一个认证中心,认证中心就是一个专门负责处理登录请求的独立的 Web 服务。

用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 Token 写入 Cookie。(注意这个 Cookie 是认证中心的,应用系统是访问不到的。)

应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统。

应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的,其他应用系统是访问不到的。)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了。

这里顺便介绍两款认证中心的开源实现:

  • Apereo CAS 是一个企业级单点登录系统,其中 CAS 的意思是”Central Authentication Service“。它最初是耶鲁大学实验室的项目,后来转让给了 JASIG 组织,项目更名为 JASIG CAS,后来该组织并入了 Apereo 基金会,项目也随之更名为 Apereo CAS。

  • XXL-SSO 是一个简易的单点登录系统,由大众点评工程师许雪里个人开发,代码比较简单,没有做安全控制,因而不推荐直接应用在项目中,这里列出来仅供参考。

总结:此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。

方法二:LocalStorage 跨域

前面,我们说实现单点登录的关键在于,如何让 Session ID(或 Token)在多个域中共享。

父域 Cookie 确实是一种不错的解决方案,但是不支持跨域。那么有没有什么奇淫技巧能够让 Cookie 跨域传递呢?

很遗憾,浏览器对 Cookie 的跨域限制越来越严格。Chrome 浏览器还给 Cookie 新增了一个 SameSite 属性,此举几乎禁止了一切跨域请求的 Cookie 传递(超链接除外),并且只有当使用 HTTPs 协议时,才有可能被允许在 AJAX 跨域请求中接受服务器传来的 Cookie。

不过,在前后端分离的情况下,完全可以不使用 Cookie,我们可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端。这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID (或 Token )放在响应体中传递给前端。

在这样的场景下,单点登录完全可以在前端实现。前端拿到 Session ID (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中。

实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取 token
var token = result.data.token;

// 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用postMessage()方法将token传递给iframe
setTimeout(function () {
iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
iframe.remove();
}, 6000);

// 在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,把接收到的token数据写入localStorage
window.addEventListener('message', function (event) {
localStorage.setItem('token', event.data)
}, false);

前端通过 iframe+postMessage () 方式,将同一份 Token 写入到了多个域下的 LocalStorage 中,前端每次在向后端发送请求之前,都会主动从 LocalStorage 中读取 Token 并在请求中携带,这样就实现了同一份 Token 被多个域所共享。

总结:此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域。