单点登录的简单理解(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

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