單點登錄的簡單理解(SSO)

實現原理

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 的異步調用實現,這樣可以避免不必要的頁面跳轉。

參考

https://www.jianshu.com/p/75edcc05acfd

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章