實現原理
1.SSO的實現主要是通過cookie機制實現,當瀏覽器與後臺服務交互時,瀏覽器會將該域名下的所有cookie鍵值攜帶到服務器,這樣服務器就可以獲得cookie的鍵值。
2.cookie的鍵值會保存的瀏覽器指定的目錄中,所以在同一次瀏覽器行爲中(沒有關閉瀏覽器)。不管在這次瀏覽行爲中,跳轉到任何頁面,當再次跳轉的原來頁面時,瀏覽器仍然能獲得上一次該域名設置的cookie (過期鍵除外),並且可以在與後臺交互時,攜帶這些cookie。
主要對象
1.各種應用
2.SSO 登錄服務器
3.cookie(登錄態標記)
實現流程
SSO 服務需要考慮的問題
1.token的隨機性(即使代碼泄露,破解成本也要足夠大)
2.token 的加密方式及防token僞造
3.token 的有效期
4.SSO服務如何實現高可用
代碼Demo
1.搭建多個應用(Application)網址,域名可以是www.app1.com/www.app2.com....., 並搭建一個應用登錄服務 www.sso.com
2.Application 代碼:
<?php
//如果不存登錄態標記,跳轉到登錄頁 app1_sess 爲app1應用的登錄態標記。
//token 是sso返回的登錄回執。
if(!isset($_COOKIE['app1_sess']) || !isset($_COOKIE['token'])){
//回跳頁面
$redirect = urlencode('http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
//跳轉登錄頁
header("location:http://www.sso.com/login.html?redirect={$redirect}");
return true;
}
/*
如果url存在登錄回執,去sso服務驗證token的有效性。
*/
if(isset($_GET['token'])){
$postData = ['token'=>$_GET['token']];
$url = "http://www.sso.com/token.php"; // 驗證token地址
$resp = [];
postCurl($postData,$url,$resp);
if($resp['errcode'] != 0){ //驗證失敗
clearAppCookie();
header("location:http://www.app1.com/error.html");
return true;
}
$data = $resp['data'];
if($data['errno']!=0){ //token無效
clearAppCookie(); //清空cookie
header("location:http://www.app1.com/error.html");
return true;
}
$uid = $data['data']['uid'];
$userInfo = getUserInfoFromDb($uid); //從DB 獲取用戶信息
//標記登錄態
$sessData = getSessData($uid);
$sessId = $sessData['sessId'];
$sessKey = $sessData['sessKey'];
//將要用戶信息保存在 APPSESS.XXX 的鍵值對中(redis)
setVauleToRedis("APPSESS.".$sessId,$sessKey);
setAppCookie("app1_sess",$sessId); //標記cookie登錄態
setAppCookie("token",sha1($sessKey)); //設置Application token(這個值用於防僞)
echo "do something"; //繼續執行頁面其他操作
return true;
}
$sessId = $_COOKIE['app1_sess'];
$sessKey = getVauleFromRedis("APPSESS.".$sessId); //根據登錄態回執,獲取用戶信息。
$token = sha1($sessKey); //加密用戶信息
//驗證登錄信息是否合法
if($token != $_COOKIE['token']){ //防僞
echo "token error";
clearAppCookie();
return false;
}
//續期登錄態(延遲登錄態的有效時間)
setVauleToRedis("APPSESS.".$sessId,$sessKey);
setAppCookie("app1_sess",$sessId);
setAppCookie("token",$token);
echo "do other many things"; //繼續執行頁面其他操作
function getSessData($uid){
$loginTime = time();
$loginTime = "1234567"; ////測試需要,寫成固定
//通過登錄時間+uid+隨機數,以保證sessKey的隨機性。
$sessKey = $loginTime."_".$uid."_"."969000"; //969000 是一個隨機數 rand(1,100000);
$sessId = md5($sessKey);
return ['sessId'=>$sessId,'sessKey'=>$sessKey];
}
//獲取用戶信息
function getUserInfoFromDb($uid){
//測試需要,寫成固定
$userInfo = [
'userId'=>1234567,
'age'=>17,
'sex'=>1
];
return $userInfo;
}
//設置KV到緩存組件
function setVauleToRedis($key,$val,$expire=86400){
return true;
}
//從緩存組件中獲取指定Key的值
function getVauleFromRedis($key){
//測試需要,寫成固定
return "1234567"."_"."1234567"."_"."969000";
}
//寫入cookie
function setAppCookie($key,$val,$expire=60){
setcookie($key, $val, time()+$expire, "/", "app1.a.com");
}
//清除Cookie
function clearAppCookie(){
}
function postCurl($postData,$url,&$responseData,$second=30)
{
$responseData = ['errmsg'=>'','errcode'=>0,'data'=>[]];
$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
//post提交方式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
$data = curl_exec($ch);
if($data){
curl_close($ch);
$responseData['data'] = json_decode($data,true);
return true;
}
$error = curl_errno($ch);
$responseData['errcode']=10001;
$responseData['errmsg'] = "curl出錯,錯誤碼:$error";
curl_close($ch);
return false;
}
?>
3.SSO登錄頁面:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<script type="text/javascript" charset="utf-8">
function getQueryVariable(variable){
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}
window.onload=function(){
var redirect = getQueryVariable("redirect"); //獲取回跳地址
console.log(redirect);
if(redirect == false){
return true;
}
var formEle = document.getElementById("login");
formEle.action = "./login.php?redirect="+redirect; //增加回跳地址
}
</script>
<body>
<form action="./login.php" method="post" id="login">
用戶:<input type="text" name="username" value=""><br>
密碼:<input type="password" name="password" value="" placeholder="輸入密碼"><br>
<input type="submit" name="" value="登錄">
</form>
</body>
</html>
4.後臺登錄頁處理(login.php):
<?php
$userInfo = [];
$isLogin = isset($_COOKIE['sessId']) && ($userInfo=getValueFromRedis($_COOKIE['sessId'])); //判斷是否登錄
$data = ['errno'=>0,'errmsg'=>''];
//如果已經登錄,更新sessId,並返回新token
if($isLogin){
$token = setSession($userInfo);
if($token == false){
$data['errno'] = 20003;
$data['errmsg'] = "系統繁忙";
} else {
$data['data'] =['token'=>$token];
}
echo json_encode($data);
return true;
}
//檢查登錄參數
if( !isset($_POST['username']) || !isset($_POST['password']) ){
$data['errno'] = 20004;
$data['errmsg'] = "參數錯誤";
echo json_encode($data);
return true;
}
$user = $_POST['username'];
$passwd = $_POST['password'];
if(empty($user) || empty($passwd)){
$data['errno'] = 20001;
$data['errmsg'] = "賬號密碼錯誤";
echo json_encode($data);
return false;
}
//匹配用戶
$checkRet = checkUser($user,$passwd,$userInfo);
if(!$checkRet){
$data['errno'] = 20002;
$data['errmsg'] = "賬號密碼錯誤";
echo json_encode($data);
return false;
}
/*
寫sessId 標記 SSO頁面的登錄態。(這裏要注意是,SSO服務器的登錄標記,而不是Application)
生成token 返回給Application ,Application 可以通過這個token 來驗證這個用戶是否在SSO服務器登錄過。
*/
setValueToRedis($sessId,$userInfo,86400); //保存用戶信息
$token = setSession($userInfo)
if($token == false){
$data['errno'] = 20003;
$data['errmsg'] = "系統繁忙";
} else {
$data['data'] =['token'=>$token];
}
if($data['errno'] !=0 ){
echo json_encode($data);
return true;
}
//如果有回跳地址,則回跳
if(isset($_GET['redirect'])){
$redirect = $_GET['redirect']."?token=".$token;
header("location:{$redirect}");
return true;
}
//跳轉默認地址
//header("location:");
return true;
function setSession($userInfo){
$uid = $userInfo["userId"]
$sessId = getSessId($uid);
setSessIdCookie($sessId); //標記登錄態
$token = getToken($uid); //生成token
if(!setVauleToRedis($token,$uid,60)){ //將token 保存在redis中,並設置token的有效期爲60秒
return false;
} else {
return $token;
}
}
function getToken($uid){
$salt = "TOKET";
$rand = rand(1,1000000);
$tm = time();
$tokenStr = $salt."_".$tm."_".$rand."_".$uid;
return md5($tokenStr);
}
function setSessIdCookie($sessId){
setcookie("sessId", $sessId, time()+60, "/", "a.com");
}
function getSessId($uid){
$loginTime = time();
$sessId = $loginTime."*".$uid;
$sessId = md5($sessId);
return $sessId;
}
/*
檢查用戶密碼是否存在;
*/
function checkUser($user,$passwd,&$userInfo){
$userInfo = [
'userId'=>123456,
'age'=>17,
'sex'=>1
];
return true; //沒有匹配用戶返回false
}
function setVauleToRedis($key,$val,$expire=86400){
return true;
}
function getValueFromRedis($key){
$userInfo = [
'userId'=>123456,
'age'=>17,
'sex'=>1
];
return $userInfo; //如果redis獲取不到鍵值,返回false
}
?>
5.SSO 服務 token驗證:
<?php
if(!isset($_POST['token'])){
$data = ['errno'=>20001,"errmsg"=>'參數錯誤','data'=>['uid'=>0]];
echo json_encode($data);
return true;
}
$uid = isLogin($_POST['token']);
if($uid === false) {
$data = ['errno'=>20002,"errmsg"=>'未登錄','data'=>['uid'=>0]];
echo json_encode($data);
return true;
}
deleteToken($token); //刪除token 避免二次使用
$data = ['errno'=>0,"errmsg"=>'','data'=>['uid'=>$uid]];
echo json_encode($data);
/*
判斷token是否存在
*/
function isLogin($token){
return "1234567"; //測試需要
}
function deleteToken($token){
}
?>
總結
1.SSO 的實現依賴cookie,只要正確理解了cookie機制,才能正確的理解 SSO 的實現機制
2.SSO 的 token 回執、登錄態標記、以及Application 的防僞token,設計時要根據業務特點,設置足夠的隨機性,以增加破解的成本。
3.登錄頁面的登錄接口可以通過 js 的異步調用實現,這樣可以避免不必要的頁面跳轉。