前言
在 B/S 系统中,登陆功能一般都是基于 Cookie 来实现的。当用户登陆成功后,通常会将登陆状态记录到 Session 中,或者是给用户签发一个 Token,不管哪种方式,都须要在客户端保存一些信息(Session ID 或 Token ),并要求客户端在以后的每次请求中携带它们。在这样的场景下,使用 Cookie 无疑是最方便的,所以咱们通常都会将 Session 的 ID 或 Token 保存到 Cookie 中,当服务端收到请求后,经过验证 Cookie 中的信息来判断用户是否登陆 。html
单点登陆(Single Sign On, SSO)是指在同一账号平台下的多个应用系统中,用户只需登陆一次,便可访问全部相互信任的应用系统。举例来讲,百度贴吧和百度地图是百度公司旗下的两个不一样的应用系统,若是用户在百度贴吧登陆过以后,当他访问百度地图时无需再次登陆,那么就说明百度贴吧和百度地图之间实现了单点登陆。前端
单点登陆的本质就是在多个应用系统中共享登陆状态。若是用户的登陆状态是记录在 Session 中的,要实现共享登陆状态,就要先共享 Session,好比能够将 Session 序列化到 Redis 中,让多个应用系统共享同一个 Redis,直接读取 Redis 来获取 Session。后端
固然仅此是不够的,由于不一样的应用系统有着不一样的域名,尽管 Session 共享了,可是因为 Session ID 是每每保存在浏览器 Cookie 中的,所以存在做用域的限制,没法跨域名传递,也就是说当用户在 app1.com 中登陆后,Session ID 仅在浏览器访问 app1.com 时才会自动在请求头中携带,而当浏览器访问 app2.com 时,Session ID 是不会被带过去的。实现单点登陆的关键在于,如何让 Session ID(或 Token)在多个域中共享。跨域
实现方式一:父域 Cookie
在将具体实现以前,咱们先来聊一聊 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 这个主域名之下,那么它们就能够经过这种方式来实现单点登陆。网络
总结:此种实现方式比较简单,但不支持跨主域名。app
实现方式二:认证中心
咱们能够部署一个认证中心,认证中心就是一个专门负责处理登陆请求的独立的 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 中。
关键代码以下:
// 获取 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 被多个域所共享。
总结:此种实现方式彻底由前端控制,几乎不须要后端参与,一样支持跨域。
补充:域名分级
从专业的角度来讲(根据《计算机网络》中的定义),.com、.cn 为一级域名(也称顶级域名),.com.cn、baidu.com 为二级域名,sina.com.cn、tieba.baidu.com 为三级域名,以此类推,N 级域名就是 N-1 级域名的直接子域名。
从使用者的角度来讲,通常把可支持独立备案的主域名称做一级域名,如 baidu.com、sina.com.cn 皆可称做一级域名,在主域名下创建的直接子域名称做二级域名,如 tieba.baidu.com 为二级域名。
为了不歧义,本人将使用“主域名“替代”一级域名“的说法。