什么是单点登录

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。SSO一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作,当一个系统成功登录以后,passport将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport发起认证。

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

单点登录流程

1.登录

  • 用户访问系统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认证中心,全局会话与局部会话有如下约束关系

  • 局部会话存在,全局会话一定存在

  • 全局会话存在,局部会话不一定存在

  • 全局会话销毁,局部会话必须销毁

2.注销

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

  • 用户向系统1发起注销请求

  • 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求

  • sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址

  • sso认证中心向所有注册系统发起注销请求

  • 各注册系统接收sso认证中心的注销请求,销毁局部会话

  • sso认证中心引导用户至登录页面

什么是CAS

CAS是Central Authentication Service的缩写,中央认证服务,一种独立开放指令协议。CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法。CAS 包含两个部分:CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。

CAS 最基本的协议过程:

CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源。对于访问受保护资源的每个 Web 请求,CAS Client 会分析该请求的 Http 请求中是否包含 Service Ticket,如果没有,则说明当前用户尚未登录,于是将请求重定向到指定好的 CAS Server 登录地址,并传递 Service (也就是要访问的目的资源地址),以便登录成功过后转回该地址。用户在第 3 步中输入认证信息,如果登录成功,CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket,并缓存以待将来验证,之后系统自动重定向到 Service 所在地址,并为客户端浏览器设置一个 Ticket Granted Cookie(TGC),CAS Client 在拿到 Service 和新产生的 Ticket 过后,在第 5,6 步中与 CAS Server 进行身份核实,以确保 Service Ticket 的合法性。在该协议中,所有与 CAS 的交互均采用 SSL 协议,确保,ST 和 TGC 的安全性。协议工作过程中会有 2 次重定向的过程,但是 CAS Client 与 CAS Server 之间进行 Ticket 验证的过程对于用户是透明的。另外,CAS 协议中还提供了 Proxy (代理)模式,以适应更加高级、复杂的应用场景,具体介绍可以参考 CAS 官方网站上的相关文档。

什么是OAuth2

OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。通俗说,OAuth就是一种授权的协议,只要授权方和被授权方遵守这个协议去写代码提供服务,那双方就是实现了OAuth模式。详细说就是,OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。OAuth2是OAuth1.0的下一个版本,OAuth2关注客户端开发者的简易性,同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。原先的OAuth,会发行一个 有效期非常长的token(典型的是一年有效期或者无有效期限制),在OAuth 2.0中,server将发行一个短有效期的access token和长生命期的refresh token。这将允许客户端无需用户再次操作而获取一个新的access token,并且也限制了access token的有效期。

CAS和OAuth2区别

  • CAS的单点登录时保障客户端的用户资源的安全,OAuth2则是保障服务端的用户资源的安全;

  • CAS客户端要获取的最终信息是,这个用户到底有没有权限访问我(CAS客户端)的资源;oauth2获取的最终信息是,我(oauth2服务提供方)的用户的资源到底能不能让你(oauth2的客户端)访问;

  • CAS的单点登录,资源都在客户端这边,不在CAS的服务器那一方。用户在给CAS服务端提供了用户名密码后,作为CAS客户端并不知道这件事。随便给客户端个ST,那么客户端是不能确定这个ST是用户伪造还是真的有效,所以要拿着这个ST去服务端再问一下,这个用户给我的是有效的ST还是无效的ST,是有效的我才能让这个用户访问。

  • OAuth2认证,资源都在OAuth2服务提供者那一方,客户端是想索取用户的资源。所以在最安全的模式下,用户授权之后,服务端并不能直接返回token,通过重定向送给客户端,因为这个token有可能被黑客截获,如果黑客截获了这个token,那用户的资源也就暴露在这个黑客之下了。于是聪明的服务端发送了一个认证code给客户端(通过重定向),客户端在后台,通过https的方式,用这个code,以及另一串客户端和服务端预先商量好的密码,才能获取到token和刷新token,这个过程是非常安全的。如果黑客截获了code,他没有那串预先商量好的密码,他也是无法获取token的。这样oauth2就能保证请求资源这件事,是用户同意的,客户端也是被认可的,可以放心的把资源发给这个客户端了。

  • CAS登录和OAuth2在流程上的最大区别就是,通过ST或者code去认证的时候,需不需要预先商量好的密码。

总结:
CAS:授权服务器,被授权客户端
  1. 授权服务器(一个)保存了全局的一份session,客户端(多个)各自保存自己的session;

  2. 客户端登录时判断自己的session是否已登录,若未登录,则(告诉浏览器)重定向到授权服务器(参数带上自己的地址,用于回调);

  3. 授权服务器判断全局的session是否已登录,若未登录则定向到登录页面,提示用户登录,登录成功后,授权服务器重定向到客户端(参数带上ticket【一个凭证号】);

  4. 客户端收到ticket后,请求服务器获取用户信息;

  5. 服务器同意客户端授权后,服务端保存用户信息至全局session,客户端将用户保存至本地session

OAuth2:主系统,授权系统(给主系统授权用的,也可以跟主系统是同一个系统),第三方系统
  1. 第三方系统需要使用主系统的资源,第三方重定向到授权系统;

  2. 根据不同的授权方式,授权系统提示用户授权;

  3. 用户授权后,授权系统返回一个授权凭证(accessToken)给第三方系统【accessToken是有有效期的】;

  4. 第三方使用accessToken访问主系统资源【accessToken失效后,第三方需重新请求授权系统,以获取新的accessToken】。

什么是JWT

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。JSON WEB令牌结构由三部分组成:

  • Header(头部):包括令牌的类型及正在使用的散列算法。

  • Payload(负载):声明是关于实体(通常是用户)和其他数据的声明。索赔有三种类型:标准注册声明,公共的声明和私有的声明。

  • Signature(签名):必须采用编码标头,编码的有效负载,秘密,标头中指定的算法,并对其进行签名。

  1. 负载-标准的声明:

  • iss:JWT签发者

  • sub:JWT所面向的用户

  • aud:接收JWT的一方

  • exp:JWT的过期时间,这个过期时间必须要大于签发时间,这是一个秒数

  • nbf:定义在什么时间之前,该JWT都是不可用的

  • iat:JWT的签发时间

  1. 负载-公共的声明:可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。

  2. 负载-私有声明:提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

创建签名需要使用编码后的headerpayload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload), secret)签名用于验证消息的发送者以及消息是没有经过篡改的。完整的JWT格式输出是以.分隔的三段Base64编码, 密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,所以需要保护好,更多信息请移步官网

单点登录关于前端的部分

此代码采用OAuth2。关于token存储问题,参考了网上许多教程,大部分都是将token存储在cookie中,然后将cookie设为顶级域来解决跨域问题,但我司业务需求是某些产品顶级域也各不相同。故实现思路是将token存储在localStorage中,然后通过H5的新属性postMessage来实现跨域共享,对跨域不了解的可以看我这篇文章。实现思路:当用户访问公司某系统(如product.html)时,在product中会首先加载一个iframe,iframe中可以获取存储在localStorage中的token,如果没有取到或token过期,iframe中内部将把用户将重定向到登录页,用户在此页面登录,仍将去认证系统取得token并保存在iframe页面的localStorage

<!--product.html-->
<head><script src="auth_1.0.0.js"></script>
</head>
<body><h2>产品页面</h2><a onClick="login()" id="login">登录</a><h3 id="txt"></h3>
</body>
<script>
var opts = {origin: 'http://localhost:8080',login_path: '/login.html',path: '/cross_domain.html'
}
// 加载iframe,将src值为cross_domain.html的iframe加载到本页
var auth = new ssoAuth(opts);
function getTokenCallback(data) {//如果没有token则跳到登录页if(!data.value){auth.doWebLogin();}//如果有token,直接在页面显示,然后做其它操作document.getElementById('txt').innerText = 'token=' + data.value;
}
// 获取存储在名为cross_domain的iframe中的token
auth.getToken(getTokenCallback);
</script>

讲解:在product.html中实例化了ssoAuth后,此页面便将iframe引入了当前页,名为opts.path的值,即cross_domain.html。auth.getToken()是获取此iframe页面中的localStorage值。

//auth_1.0.0.js
function ssoAuth(opts) {this._origin = opts.origin,this._iframe_path = opts.path,this._iframe = null,this._iframe_ready = false,this._queue = [],this._auth = {},this._access_token_msg = { type: "get", key: "access_token" },this._callback = undefined,that = this;//判断是否支持postMessage及localStoragevar supported = (function () {try {return window.postMessage && window.JSON && 'localStorage' in window && window['localStorage'] !== null;} catch (e) {return false;}})();_iframeLoaded = function () {that._iframe_ready = trueif (that._queue.length) {for (var i = 0, len = that._queue.length; i < len; i++) {_sendMessage(that._queue[i]);}that._queue = [];}}_sendMessage = function (data) {// 通过contentWindow属性,脚本可以访问iframe元素所包含的HTML页面的window对象。that._iframe.contentWindow.postMessage(JSON.stringify(data), that._origin);}//获取token,但因为此时iframe还没有加载完成,先将消息存储在队列_queue中this._auth.getToken = function (callback) {that._callback = callbackif (that._access_token_msg && that._iframe_ready) {//当iframe加载完成,给iframe所在的页面发送消息_sendMessage(that._access_token_msg);} else {that._queue.push(that._access_token_msg);}}var _handleMessage = function (event) {if (event.origin === that._origin) {var data = JSON.parse(event.data);if (data.error) {console.error(event.data)that._callback({ value: null });return;}if (that._callback && typeof that._callback === 'function') {that._callback(data);} else {console.error("callback is null or not a function, please ");}}}this._auth.doWebLogin = function () {window.location.href = opts.origin + opts.login_path + "?redirect_url=" + window.location.href}//初始化了一个iframe,并追加到父页面的底部if (!this._iframe && supported) {this._iframe = document.createElement("iframe");this._iframe.style.cssText = "position:absolute;width:1px;height:1px;left:-9999px;";document.body.appendChild(this._iframe);if (window.addEventListener) {this._iframe.addEventListener("load", function () {_iframeLoaded();}, false);window.addEventListener("message", function (event) {_handleMessage(event)}, false);} else if (this._iframe.attachEvent) {this._iframe.attachEvent("onload", function () {_iframeLoaded();}, false);window.attachEvent("onmessage", function (event) {_handleMessage(event)});}this._iframe.src = this._origin + this._iframe_path;}return this._auth;
}
<!--cross_domain.html-->
<script type="text/javascript">(function () {//白名单var whitelist = ["localhost", "127.0.0.1", "^.*\.domain\.com"];function verifyOrigin(origin) {var domain = origin.replace(/^https?:\/\/|:\d{1,4}$/g, "").toLowerCase(),i = 0,len = whitelist.length;while (i < len) {if (domain.match(new RegExp(whitelist[i]))) {return true;}i++;}return false;}function handleRequest(event) {// 白名单较验if (verifyOrigin(event.origin)) {var request = JSON.parse(event.data);if (request.type == 'get') {var idi = localStorage.getItem("idi");if (!idi) {// source:对发送消息的窗口对象的引用,event.source只是window对象的代理,不能通过它访问window//的其它信息event.source.postMessage(JSON.stringify({ key: request.key, value: null }), event.origin);return;}value = JSON.parse(idi)[request.key];event.source.postMessage(JSON.stringify({ key: request.key, value: value }), event.origin);} else {event.source.postMessage(JSON.stringify({ error: "Not supported", error_description: "Not supported message type" }), event.origin);}}}// 接收iframe传来的消息if (window.addEventListener) {window.addEventListener("message", handleRequest, false);} else if (window.attachEvent) {window.attachEvent("onmessage", handleRequest);}})();
</script>
<!--login.html-->
<head><script src="auth_1.0.0.js"></script>
</head>
<body><form><input type="text" placeholder="用户名" id="user"><input type="password" placeholder="密码" id="pwd"></form><button onClick="login()">登 录</button>
</body>
<script>function login() {var name = document.getElementById('user')var pwd = document.getElementById('pwd')var expires_in = 7200//假如这是登录成功后,后台开发人员返回的json数据var res = { access_token: "xxxxx.yyyyy.zzzzz", expires_at: expires_in * 1000 + new Date().getTime(), refresh_token: "yyyyyyyyyyyyyyyyyyyyyyyyyyyy" };localStorage.setItem("idi", JSON.stringify(res))//登录成功后再返回原页面window.location.href = getQueryString("redirect_url")}function getQueryString(name) {var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");var r = window.location.search.substr(1).match(reg);if (r != null) return unescape(r[2]); return null;}
</script>

PS:注销暂时没做。另外postMessage有兼容性问题,如果其它小伙伴有更好的方法,望分享一下,谢谢~

  • 本文作者:An_an

  • 本文链接:https://juejin.cn/post/6844903664264413198

1. JavaScript 重温系列(22篇全)

2. ECMAScript 重温系列(10篇全)

3. JavaScript设计模式 重温系列(9篇全)

4. 正则 / 框架 / 算法等 重温系列(16篇全)

5. Webpack4 入门(上)|| Webpack4 入门(下)

6. MobX 入门(上) ||  MobX 入门(下)

7. 120+篇原创系列汇总

回复“加群”与大佬们一起交流学习~

点击“阅读原文”查看 120+ 篇原创文章

【Web技术】919- 前端关于单点登录的知识相关推荐

  1. 如何使用 WEB 技术编写前端代码,实现大屏展示和地图显示功能

    使用 WEB 技术编写前端代码实现大屏展示和地图显示功能可以采用以下步骤: 使用 HTML.CSS.JavaScript 等前端技术构建页面布局和样式. 使用 JavaScript 库或框架,如 jQ ...

  2. web工程中集成cas单点登录

    背景 cas的服务端已经搭建成功,现在需要在web项目中集成cas client.我们项目使用spring 1.在pom中配置 <!--cas单点登录 --><dependency& ...

  3. 【基于唯品会MP平台】集群环境下session共享技术方案及分布式单点登录

    背景 传统的B/S架构的系统中,一般为单点部署,并不存在集群,所以也不存在session丢失的问题.那么,由于单点部署一旦宕机,无法保证系统可用性,那我们就想到把它扩展为多台服务器部署,这样既保证了系 ...

  4. 配置NetCool/OMNIBus Web GUI与ITM TEPS单点登录

    环境: TIP(Web GUI)版本 7.3.1 ITM版本 6.2.2.2 TDS版本 安装配置Object Server(略) 安装配置TIP (Web GUI 略) 安装配置ITM(略) 安装配 ...

  5. 前端React单点登录的实现

    场景: 在多个系统进行嵌套的情况下,从父系统跳转到子系统时,无需再次登录直接跳转; 原理:在子系统登录页判断url的内容通过location属性,拿到url后面的参数,通过参数调用后端接口实现单点登录 ...

  6. 【Web技术】前端水印实现方案

    一.问题背景 为了防止信息泄露或知识产权被侵犯,在web的世界里,对于页面和图片等增加水印处理是十分有必要的,水印的添加根据环境可以分为两大类,前端浏览器环境添加和后端服务环境添加,简单对比一下这两种 ...

  7. VUE 前端SSO单点登录

    前端SSO实现 主要设计思路 独立的登陆页面项目 子系统axios设置 主登陆页代码 主要难点 主要设计思路 独立的登陆页面项目 ,所有子系统的公共登陆页: axios请求拦截 ,子系统要做请求拦截, ...

  8. Web技术,制作简易的登录界面

    登录界面 首先需要了解登录界面中分为几个部分(分板块后,设置div,方便操作),我们将其分为三个部分 第一步,制定框架: 1.首先在body部分设置出一个div模块,用于在界面中显示出一个登录的界面框 ...

  9. 前端051_单点登录SSO_注册功能实现

    注册功能实现 1.EasyMock 添加注册接口 2.定义 Api 调用用户协议和注册接口 3.提交注册数据 4.测试 1.EasyMock 添加注册接口 查询用户名是否已被注册 请求URL: /sy ...

最新文章

  1. 【组队学习】【29期】Datawhale组队学习内容介绍
  2. IntelliJ IDEA 环境常用设置整理
  3. eclipse上配置Maven
  4. Windows和Linux环境下搭建SVN服务器
  5. Firefox 下载、附加组件、Flash插件、缓存位置(附加Chrome下载和Opera下载)
  6. Tengine HTTPS原理解析、实践与调试
  7. 利用反射自动封装成实体对象
  8. Class文件结构amp;字节码指令
  9. pandas corr()函数
  10. 如何让html 兼容IE和chrome,IE Tab(让Chrome兼容IE)
  11. 使用moment获取本周、前n周、后n周开始结束日期以及动态计算周数
  12. 阿里云网盘内测申请_最新阿里云网盘官方申请地址,哪里可以获得阿里网盘内测码?9月23日截至...
  13. 模拟电子电路(1)——概论
  14. 英国内政部(Home Office)间谍机构(spy powers)假装它是Ofcom咨询中的一名私人公民1525446049260...
  15. 按照姓名拼音首字母排序
  16. 嵌入式之uboot源码分析-启动第一阶段学习笔记
  17. 基于神经网络的文字识别,神经网络如何识别图像
  18. 尼日利亚4g频段_4G全球频段划分及主要运营商对应表2015版.xls
  19. 利用Python在互动吧网站自动抢票实战分析!你学废了吗
  20. IOC和DI到底是什么?

热门文章

  1. java ee7权威指南 卷1,JavaEE7权威指南,卷1(原书第5版)中文pdf
  2. Mac Xcode崩溃 (打开ios项目引起崩溃)
  3. 睡眠键重启计算机,Windows10进入睡眠模式后按任意键会自动重启怎么办
  4. python余弦相似度_推荐系统01--余弦相似度
  5. 基于和芯星通UM482的RTK差分定位
  6. css自动转rem,css之px自动转rem—“懒人”必备
  7. python五角星教程_绘制五角星_清华尹成python入门教程_少儿编程视频-51CTO学院
  8. Linux下vi修改行距,调整label中text显示的行间距
  9. Commons-Collections3 , Commons-Collections4反序列化 从0开始手写exp
  10. 餐馆点餐系统(Java GUI + mysql)