前端關於單點登陸的知識

什麼是單點登陸

單點登陸(Single Sign On),簡稱爲 SSO,是目前比較流行的企業業務整合的解決方案之一。SSO的定義是在多個應用系統中,用戶只須要登陸一次就能夠訪問全部相互信任的應用系統。javascript

SSO通常都須要一個獨立的認證中心(passport),子系統的登陸均得經過passport,子系統自己將不參與登陸操做,當一個系統成功登陸之後,passport將會頒發一個令牌給各個子系統,子系統能夠拿着令牌會獲取各自的受保護資源,爲了減小頻繁認證,各個子系統在被passport受權之後,會創建一個局部會話,在必定時間內能夠無需再次向passport發起認證。css

舉個例子,好比淘寶、天貓都屬於阿里旗下的產品,當用戶登陸淘寶後,再打開天貓,系統便自動幫用戶登陸了天貓,這種現象背後就是用單點登陸實現的。html

單點登陸流程

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。java

CAS 最基本的協議過程:算法

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 官方網站上的相關文檔。

什麼是OAuth2

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的有效期。緩存

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及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有兼容性問題,若是其它小夥伴有更好的方法,望分享一下,謝謝~

參考:

相關文章
相關標籤/搜索