1. 程式人生 > >單點登入原理及實現(共享)

單點登入原理及實現(共享)

單點登入原理及實現

隨著業務發展,公司業務會不斷壯大,每個業務都會存在使用者登入和許可權驗證,不可能要求使用者每個業務網站都登入一次,這個時候,就需要單點登入功能。下面將先介紹基本概念,然後以百度(baidu.com)為例進行講解,最後用一個小例子講解如何實現(後臺用node)。本文是假設讀者對http協議等有基本瞭解。

什麼是單點登入?

單點登入即使用者在公司的某個網站登入後,訪問該公司相關的網站都是已登入狀態,無需再進行登入操作。例如我們登入百度首頁(https://www.baidu.com/ )後,訪問百度知道(https://zhidao.baidu.com/ ),就已經登入,無需再次登入該站點,這就是單點登入。這個功能很重要,以阿里巴巴來說,業務十分龐大,如果每個業務網站都要重新登入,恐怕使用者要急眼。 要實現單點登入,就要驗證該使用者已登入,問題就回歸到使用者的登入驗證問題,如何來確保該使用者已登入。

使用者登入和許可權驗證

先來看下服務是如何進行登入和許可權驗證的。我們登入一個網頁,進行操作,多個http請求如下所示: 整個登入、許可權驗證、退出操作可以分為下面幾個步驟: 1、使用者輸入使用者名稱和密碼,客戶端將使用者名稱和加密後的密碼傳送至服務端。 2、服務端成功驗證後,會生成一個加密的會話id,把放入id列表,同時,伺服器會返回請求,並在返回頭中設定setCookie,讓id儲存在cookie中。 3、使用者傳送其它請求(請求會攜帶cookie中的id)至服務端,例如獲取歷史操作記錄,查詢客戶賬單等等。 4、服務端驗證id的有效性,有效則返回正確資訊。 5、使用者傳送退出登入操作。 6、服務端驗證id,驗證成功則從id列表中刪除該id,或把該id置為過期。 7、使用者再次發出查詢賬單請求。 8、服務端驗證id,發現該id不存在或已過期,則提示使用者重新登入。 從上述過程可以發現,使用者的許可權驗證,就是會話id的生成和校驗,只要多個站點可以共享這個id,既可以完成單點登入為什麼這麼說呢?原因如下: 假設公司有兩個站點,

aaa.com和bbb.com,按照現在的架構設計,會把公共的業務模組抽象出來,作為獨立的服務。客戶資訊作為一個公共功能,自然是獨立的。架構設計圖如下所示: aaa.com和bbb.com有獨立的伺服器,但向客戶資訊系統等公共模組是獨立的,即客戶端請求資訊是,兩個服務首相會向客戶資訊管理系統進行客戶許可權驗證,通過驗證才繼續操作,返回客戶需要的資訊。 之前我們已經的出結論,使用者的許可權驗證,就是會話id的驗證。如果兩個站點的會話id共享,只需登入一次,兩個站點的許可權驗證都可以完成。即單點登入的關鍵在於兩個站點如何實現會話id(cookie)的共享,下面,我們以百度為例,分析他們如何實現cookie共享的。

例項分析

以百度為例,我們登入百度首頁(https://www.baidu.com/ )後,進入百度文庫(https://wenku.baidu.com/ )、百度知道(https://zhidao.baidu.com/ )都是已登入狀態,我們用chrome除錯工具分別看到幾個站點的cookie,發現有部分cookie是相同的。實際上,baidu.com為一級域名,其它都是二級域名,cookie可以在二級域名間共享,只需對cookie的domain進行設定即可。 上面這種情況比較可以共享cookie,如果你們公司的各個業務站點一級域名一致,可以使用該方法實現單點登入。但是,當站點之前不是同域,以及域名也不同時,是無法共享的。這是瀏覽器的同源策略引起的,目的是保護使用者資訊保安,防止不法分子竊取客戶資訊,偽裝使用者進行操作。 我們以百度(https://www.baidu.com/ ),hao123(https://www.hao123.com/ )兩個站點為例進行測試,看看他們是如何實現單點登入的。實驗步驟如下: 1、清除兩個站點的cookie和登入資訊,使他們都處於非登入狀態。 2、開啟百度首頁(https://www.baidu.com/ )並登入,檢視使用者資訊。 3、開啟hao123(https://www.hao123.com/ )站點,檢視使用者資訊。 4、比對使用者資訊,發現使用者資訊一致。即二者實現了單點登入,只要登入百度,即可登入hao123網站。 我們檢視二者cookie,發現兩者cookie中有多個值相同(例如都存在BDUSS這個值,且值相同),可以猜測他們可能共用了一套使用者資訊系統,至少使用者登入資訊是有共享的。 在登入是,我用fiddler(抓包工具,可自行百度)進行抓包,看看他們到底進行了什麼操作?百度的登入過程,抓包如下 通過分析,我在圖中用畫了幾道紅線,開始第一個是百度賬號的登入驗證,然後返回了一個加密的使用者身份唯一標識(bdu),然後向hao123站點發起請求,該請求攜帶使用者身份標識(bdu),hao123驗證該標識後,返回會話id,並存在的hao123的域cookie中。如下圖所示: 如圖所示,單點登入過程可分為以下幾個步驟: 1、使用者李三開啟aaa.com網站,輸入使用者名稱密碼,傳送登入請求至aaa.com伺服器。 2、aaa.com伺服器向客戶資訊管理中心發起請求,驗證李三使用者名稱密碼。 3、驗證完後返回會話id和使用者唯一標識(一串加密的字串,用於跨域驗證,暫時稱它為sid)。 4、aaa.com收到登入成功資訊和sid後,攜帶sid向bbb.com伺服器發起認證請求。 5、bbb.com伺服器同樣向客戶資訊管理中心發起唯一標識認證。 6、客戶資訊管理中心認證成功返回會話id給bbb.com伺服器。 7、bbb.com伺服器返回會話id,會話id會儲存在域bbb.com的cookie中。 8、後續訪問bbb.com網站,請求都會攜帶會話id,後臺拿到id去認證。 這就是單點登入的整個過程。

具體實現

下面用node模擬兩個站點進行單點登入模擬。 伺服器1,有一個頁面,兩個介面。 登入頁程式碼如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>登入頁面</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        div {
            height: 30px;
            width: 300px;
            margin: 20px auto; 
            text-align: center
        }

        div > label {
            display: inline-block;
            width: 80px;
            text-align: right;
            padding-right: 15px;
            box-sizing: border-box
        }
        div > input {
            width: 200px
        }
    </style>
</head>
<body onload="addEvent()">
    <div>
        <label>userName:</label>
        <input type="text" id="user_name">
    </div>
    <div>
        <label>password</label>
        <input type="password" id="password">
    </div>
    <div>
        <input type="button" id="submit" value="logon" style="width: 100px">
    </div>
    <script>
        //新增按鈕事件
        function addEvent() {
            let submit = document.getElementById('submit');
            if (submit) {
                submit.addEventListener('click', () => {
                    login();
                });
            }
          
        }

        //登入請求方法
        function login() {
            let userName = document.getElementById('user_name');
            let password = document.getElementById('password');
            $.ajax({
                url: '/login',
                method: 'GET',
                data: {
                    userName: userName.value,
                    password: password.value
                },
                success: function(result) {
                    //使用使用者唯一標識進行跨域認證
                    crossdomain(result.id);
                    location.href = location.protocol + '//' + location.host + '/success';
                },
                error: function(error) {
                    console.error('登入錯誤');
                }
            })
        }

        //跨域使用者認證請求
        function crossdomain(id) {
            let iframe = document.createElement('iframe');
            iframe.src = 'http://localhost:10000/crossdomain?id=' + id;
            iframe.style.display = 'none';
            document.body.appendChild(iframe);
        }
    </script>
    <script src="./jquery.js"></script>
</body>
</html>

服務端程式碼如下:

const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');

app.use(cookieParser());

//未登入訪問帶介面返回“無許可權”,登入後返回“站點3000登入成功”
app.get('/success', (req, res) => {
    let sessionId = req.cookies ? req.cookies.sessionId : '';
    if (sessionId && sessionId === '123456') {
        res.send('站點3000登入成功');
    } else {
        res.send('無許可權');
    }
});

//登入介面,驗證成功返回會話id和使用者登入唯一標識
app.get('/login', (req, res) => {
    let name = req.query.userName;
    let password = req.query.password;
    if (name === 'user' && password === '123456') {
        res.cookie('sessionId', '123456', { expires: new Date(Date.now() + 10 * 1000)});
        res.send({
            id: '123456789'
        });
    } else {
        res.sendStatus(400);
    }
});

//靜態資源訪問路徑,路由中包含/static
app.use('/static', express.static('./resources'));

app.listen(3000, () => console.log('Example app listening on port 3000!'))

伺服器2只有兩個介面,程式碼如下:

const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');
app.use(cookieParser());

//對於已認證使用者,返回‘站點10000登入成功’,反之,返回‘無許可權’
app.get('/getcontent', (req, res) => {
    let sessionId = req.cookies ? req.cookies.sessionId : '';
    if (sessionId && sessionId === '123456') {
        res.send('站點10000登入成功');
    } else {
        res.send('無許可權');
    }
});

//認證使用者介面,根據使用者唯一標識認證使用者是否登入
app.use('/crossdomain', (req, res) => {
    let id = req.query.id;
    if (id === '123456789') {
        res.cookie('sessionId', '123456', { expires: new Date(Date.now() + 1000 * 1000)});
        res.send('有許可權');
    } else {
        res.send('無許可權');
    }
});

//靜態資源訪問路徑,路由中包含/static
app.use('/static', express.static('./resources'));

app.listen(10000, () => console.log('Example app listening on port 10000!'))