前一段时间看过CAS实现跨域单点登录SSO的方案,觉得比较复杂,想到如果有了CORS是不是可以直接使用跨域ajax带session实现SSO呢?用PHP试验了一下,还真的可以。看懂接下来的流程和实现,需要有一些js,html,PHP,ajax CORS跨域的一些相关知识。
CAS的SSO方案本文就不再赘述,想了解的老铁可以另行查找相关资料,下面是整个实现流程:
流程逻辑图
测试环境
有两个域名(此处为了简化只演示一个业务系统,有需要可以添加多个b.com,c.com..逻辑都一样):
sso.com(单点登录系统)
a.com(业务系统A)
1、a.com主页代码
index.php
<?php
session_start();
//模拟业务系统登录状态查看
if(!empty($_SESSION["isLogin"])){
echo "用户".$_SESSION["username"]." ,已登录";
}else{
echo "未登录";
}
a.com/index.php,这里简化为只验证session登录状态
第一次访问结果是未登录状态:
2、sso.com前端登录页面(sso.com/index.html)
一个表单输入用户名,一个按钮触发登录逻辑
<body>
<h1>sso</h1>
<form>
用户名:<input id="userInput" type="text" name="username">
</form>
<button id="loginBtn">登录</button>
</body>
页面访问:
假设输入用户名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接受登录请求的代码如下:
<?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)返回给前端。
浏览器调试---------
发送的验证信息:
收到的返回,带token和业务系统验证token地址:
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的代码:
<?php
//跨域
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跨域头
发送到业务系统的token
业务系统返回结果
4、直接访问业务系统a.com/index.php
发现已经在登录状态了,整个sso流程结束
结束语:
对比传统的CAS,我觉得有以下一些优点:
- 步骤更少,逻辑简单
- token不写入cookie, 只在前端3步临时变量存储,随后释放,更安全
- sso服务器登录后,再访问其他业务系统,不需要redirect跳转,用户体验好
缺点:必须使用支持CORS跨域的浏览器访问,如IE10之前的老浏览器不支持
注意:
- 本文为原创,代码可以随意使用,无版权,转载请注明原地址
- 本文代码不能直接用在生产环境,只用作流程演示,如需使用需要修改增加安全性,考虑注销流程等等
- 本例公共存储中,为了演示方便存储了用户名,实际使用中可以存储数据库用户表中记录主键id等等,方便业务系统关联用户和session。
- 实际使用中需考虑各个业务系统和sso系统的session过期问题。
- 本文前端代码在
Chrome 77
内核的浏览器下运行正常,其他浏览器兼容性本文不做探讨