实现原理
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 的异步调用实现,这样可以避免不必要的页面跳转。