前一段時間看過CAS實作跨域單點登入SSO的方案,覺得比較複雜,想到如果有了CORS是不是可以直接使用跨域ajax帶session實作SSO呢?用PHP試驗了一下,還真的可以。看懂接下來的流程和實作,需要有一些js,html,PHP,ajax CORS跨域的一些相關知識。
CAS的SSO方案本文就不再贅述,想了解的老鐵可以另行查找相關資料,下面是整個實作流程:
流程邏輯圖

image.png
測試環境
有兩個域名(此處為了簡化隻示範一個業務系統,有需要可以添加多個b.com,c.com..邏輯都一樣):
sso.com(單點登入系統)
a.com(業務系統A)
1、a.com首頁代碼
index.php
session_start();
//模拟業務系統登入狀态檢視
if(!empty($_SESSION["isLogin"])){
echo "使用者".$_SESSION["username"]." ,已登入";
}else{
echo "未登入";
}
a.com/index.php,這裡簡化為隻驗證session登入狀态
第一次通路結果是未登入狀态:

1.PNG
2、sso.com前端登入頁面(sso.com/index.html)
一個表單輸入使用者名,一個按鈕觸發登入邏輯
sso
使用者名:
登入
頁面通路:

2.PNG
假設輸入使用者名user, 點選登入按鈕觸發使用者登入邏輯,ajax發送使用者名到sso.com/login.php。
登入邏輯的js代碼如下:
//-------登入Ajax請求 START
let btn = document.getElementById("loginBtn");
btn.addEventListener('click', ()=>{
let xhr = new XMLHttpRequest();
xhr.open('POST', 'login.php');
xhr.setRequestHeader('Content-Type', "application/json");
xhr.onload = function(){
//接收到sso.com/login.php的傳回
let res = JSON.parse(xhr.response);
console.log(res);
//發送跨域驗證token請求到所有業務系統
for(let serverAddr of res.servers){
loginAll(res.token, serverAddr);
}
};
let username = document.getElementById("userInput").value;
let userInfo = {username: username};
xhr.send(JSON.stringify(userInfo))
});
//-------登入Ajax請求 END
先不管loginAll函數,這個是ajax跨域到業務系統的代碼
3、sso.com/login.php接受登入請求的代碼如下:
//此處省略使用者驗證直接登入成功
//.....
//擷取使用者資訊
$userInfo = json_decode(file_get_contents("php://input"), true);
//生成token
$token = genToken();
//存儲token和使用者名
save_user_token($userInfo["username"], $token);
//發送token回前端
echo json_encode([
"token"=>$token,
//業務系統驗證token位址,此處隻示範一個,多個業務系統添加到數組即可
"servers"=>["http://a.com/check.php"]
]);
//寫入token,使用者資訊到存儲,此處用檔案示範,也可用mysql,redis等替代
function save_user_token(string $username, string $token){
$tokenMap = [];
if(file_exists("../test.txt")){
$file_content = file_get_contents("../test.txt");
$tokenMap = unserialize($file_content);
}
$tokenMap[$token] = $username;
file_put_contents("../test.txt", serialize($tokenMap));
}
//生成随機token
function genToken(){
//随機生成10位字元串token
$strs="QWERTYUIOPASDFGHJKLZXCVBNM1234567890qwertyuiopasdfghjklzxcvbnm";
$token=substr(str_shuffle($strs),mt_rand(0,strlen($strs)-11),10);
return $token;
}
這段代碼的作用就是驗證使用者登入(簡化為總是成功),生成一個token,存儲起來,并把token和所有業務系統校驗token位址(這裡簡化為一個http://a.com/check.php)傳回給前端。
浏覽器調試---------
發送的驗證資訊:

image.png
收到的傳回,帶token和業務系統驗證token位址:

image.png
3、sso.com/index.html接收到token,執行ajax跨域登入到業務系統
補充上面缺少的loginAll(res.token, serverAddr);代碼中的loginAll函數:
//跨域登入一個業務系統
function loginAll(token, address){
let xhr = new XMLHttpRequest();
xhr.open('POST', address);
xhr.withCredentials = true;
//這裡Content-Type如果是json會觸發非簡單跨域,此處為了簡化不使用
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.onload = function(){
// 收到驗證傳回
let str = xhr.response;
console.log(str);
};
let tokenObj = {token: token};
xhr.send(JSON.stringify(tokenObj));
}
業務系統a.com/check.php驗證token的代碼:
//跨域
header("Access-Control-Allow-Origin: http://sso.com");
// 響應類型
header('Access-Control-Allow-Methods:POST,GET');
// 帶 cookie 的跨域通路
header('Access-Control-Allow-Credentials: true');
// 響應頭設定
//header('Access-Control-Allow-Headers:x-requested-with,Content-Type,X-CSRF-Token');
session_start();
//擷取post資料
$tokenInfo = json_decode(file_get_contents("php://input"), true);
//傳回驗證結果
$username = check_token($tokenInfo["token"]);
$res = [];
//驗證成功設定登入session
if($username){
//把使用者資訊和登入表示寫入session
$_SESSION["isLogin"] = true;
$_SESSION["username"] = $username;
$res["res"] = "驗證成功";
}else{
$res["res"] = "驗證失敗";
}
//傳回給sso的前端
echo json_encode($res);
//從存儲中取出token并檢查
function check_token(string $token){
if(!file_exists("../test.txt")){
return false;
}
$file_content = file_get_contents("../test.txt");
$tokenMap = unserialize($file_content);
if(!key_exists($token, $tokenMap)){
return false;
}
return $tokenMap[$token];
}
浏覽器調試:
伺服器傳回帶cookie跨域頭

image.png
發送到業務系統的token

image.png
業務系統傳回結果

image.png
4、直接通路業務系統a.com/index.php
發現已經在登入狀态了,整個sso流程結束

image.png
結束語:
對比傳統的CAS,我覺得有以下一些優點:
步驟更少,邏輯簡單
token不寫入cookie, 隻在前端3步臨時變量存儲,随後釋放,更安全
sso伺服器登入後,再通路其他業務系統,不需要redirect跳轉,使用者體驗好
缺點:必須使用支援CORS跨域的浏覽器通路,如IE10之前的老浏覽器不支援
注意:
本文為原創,代碼可以随意使用,無版權,轉載請注明原位址
本文代碼不能直接用在生産環境,隻用作流程示範,如需使用需要修改增加安全性,考慮登出流程等等
本例公共存儲中,為了示範友善存儲了使用者名,實際使用中可以存儲資料庫使用者表中記錄主鍵id等等,友善業務系統關聯使用者和session。
實際使用中需考慮各個業務系統和sso系統的session過期問題。
本文前端代碼在Chrome 77核心的浏覽器下運作正常,其他浏覽器相容性本文不做探讨