單點登陸(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 Client 與受保護的客戶端應用部署在一塊兒,以 Filter 方式保護受保護的資源。對於訪問受保護資源的每一個 Web 請求,CAS Client 會分析該請求的 Http 請求中是否包含 Service Ticket,若是沒有,則說明當前用戶還沒有登陸,因而將請求重定向到指定好的 CAS Server 登陸地址,並傳遞 Service (也就是要訪問的目的資源地址),以便登陸成功事後轉回該地址。用戶在第 3 步中輸入認證信息,若是登陸成功,CAS Server 隨機產生一個至關長度、惟1、不可僞造的 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 官方網站上的相關文檔。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有兼容性問題,若是其它小夥伴有更好的方法,望分享一下,謝謝~