单点登陆(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只须要登陆一次就能够访问全部相互信任的应用系统。javascript
SSO通常都须要一个独立的认证中心(passport),子系统的登陆均得经过passport,子系统自己将不参与登陆操做,当一个系统成功登陆之后,passport将会颁发一个令牌给各个子系统,子系统能够拿着令牌会获取各自的受保护资源,为了减小频繁认证,各个子系统在被passport受权之后,会创建一个局部会话,在必定时间内能够无需再次向passport发起认证。css
举个例子,好比淘宝、天猫都属于阿里旗下的产品,当用户登陆淘宝后,再打开天猫,系统便自动帮用户登陆了天猫,这种现象背后就是用单点登陆实现的。html
用户登陆成功以后,会与sso认证中心及各个子系统创建会话,用户与sso认证中心创建的会话称为全局会话,用户与各个子系统创建的会话称为局部会话,局部会话创建以后,用户访问子系统受保护资源将再也不经过sso认证中心,全局会话与局部会话有以下约束关系前端
CAS是Central Authentication Service的缩写,中央认证服务,一种独立开放指令协议。CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登陆方法。CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 须要独立部署,主要负责对用户的认证工做;CAS Client 负责处理对客户端受保护资源的访问请求,须要登陆时,重定向到 CAS Server。java
CAS 最基本的协议过程:算法
OAuth(开放受权)是一个开放标准,容许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。json
通俗说,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的有效期。缓存
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,能够在各方之间做为JSON对象安全地传输信息。此信息能够经过数字签名进行验证和信任。JWT可使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
JSON WEB令牌结构由三部分组成:
建立签名须要使用编码后的
header
和payload
以及一个秘钥,使用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及localStorage
var supported = (function () {
try {
return window.postMessage && window.JSON && 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
})();
_iframeLoaded = function () {
that._iframe_ready = true
if (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 = callback
if (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 = sessionStorage.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有兼容性问题,若是其它小伙伴有更好的方法,望分享一下,谢谢~