單點登入原理及實現(共享)
單點登入原理及實現
隨著業務發展,公司業務會不斷壯大,每個業務都會存在使用者登入和許可權驗證,不可能要求使用者每個業務網站都登入一次,這個時候,就需要單點登入功能。下面將先介紹基本概念,然後以百度(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,既可以完成單點登入為什麼這麼說呢?原因如下:
假設公司有兩個站點,
例項分析
以百度為例,我們登入百度首頁(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!'))