使用背景
1.什么时候需要考虑使用单点登陆?
2.单点登陆对用户数据要求是什么?
1.SSO单点登陆介绍
SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一。
sso主要分为:
1.同域 SSO(多应用同一个域名)
2.同父域SSO (多应用主域名一致)
3.跨域SSO (多应用主域名不一致)
待讨论问题:
1.单点登陆系统 数据一致性问题 多用户中心还是单用户中心
2.单点登陆的实现方案
2.1 使用cookie共享实现单点登陆(同父域SSO)
本地使用a.sso.com和 b.sso.com 分别解析到 对应的目录
每个目录下面三个文件 分别为 login.php index.php logout.php
<?php
//login.php
$username = isset($_COOKIE['username']) ? $_COOKIE['username'] : null;
if ($username){
header("location: http://a.sso.com/index.php");
}
if ( !empty($_POST['username']) && !empty($_POST['password']) &&
$_POST['username'] == "test" && $_POST['password'] == "123456" ){
$isLogin = true;
//设置cookie域名为主域名
setcookie("username", 'test', time()+3600, "/", "sso.com");
header("location: http://a.sso.com/index.php");
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>A站点</title>
</head>
<body>
<h2>A站点</h2>
<form action="/login.php" method="post" enctype="application/x-www-form-urlencoded" >
<div>
用户名:<input type="text" name="username" value="test">
</div>
<div>
密码:<input type="text" name="password" value="123456">
</div>
<div>
<button>登陆</button>
</div>
</form>
</body>
</html>
//index.php
$username = isset($_COOKIE['username']) ? $_COOKIE['username'] : null;
if ($username){
echo "<h2>A站点</h2>";
echo "<h2>".$username."已经登陆<a href='/logout.php'>退出</a></h2>";
}else{
header("location: http://a.sso.com/login.php");
}
//logout.php
setcookie("username", null, time()-1, "/", "sso.com");
header("location: http://a.sso.com/login.php");
b站点下面的代码基本一致 最终可以实现 2两个站点的同步登陆和退出的操作


该方案总结
- .登陆信息直接存cookie里面 不是很安全 可以进行复杂的加密处理 (或者可以使用jwt)
- 如果要使用session存储,通过session共享存数据库或者缓存服务器也可实现。
- 该方案不能实现不同主域名的单点登陆
- 该方案的登陆也可以单独进行部署
2.2 使用P3P协议实现跨域设置cookie(跨域SSO)
P3P是什么
P3P(Platform for Privacy Preferences)是W3C公布的一项隐私保护推荐标准,以为用户提供隐私保护。 P3P标准的构想是:Web 站点的隐私策略应该告之访问者该站点所收集的信息类型、信息将提供给哪些人、信息将被保留多少时间及其使用信息的方式,如站点应做诸如 “本网站将监测您所访问的页面以提高站点的使用率”或“本网站将尽可能为您提供更合适的广告”等申明。访问支持P3P网站的用户有权查看站点隐私报告,然 后决定是否接受cookie 或是否使用该网站。
利用P3P实现跨域
有别于JS跨域、IFRAME跨域等的常用处理办法,通过发送P3P头信息而实现的跨域。
PHP 使用P3P协议
header('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"');

通过以上介绍可以看出 部分浏览器支持跨域的cookie设置 使用p3p可以解决cookie的跨域设置从而实现跨域的单点登陆 目前Ucenter就是采用这种模式。
所以对第一种方法代码进行简单的修改 分别配置 a.sso1.com 和 a.sso2.com对应各自的目录,每个目录下面的文件为 index.php login.php logout.php
a.sso1.com下面的代码如下:
//index.php
<?php
$username = isset($_COOKIE['username']) ? $_COOKIE['username'] : null;
if ($username){
echo "<h2>A站点</h2>";
echo "<h2>".$username."已经登陆<a href='/logout.php'>退出</a></h2>";
}else{
header("location: http://a.sso1.com/login.php");
}
<?php
//login.php
$username = isset($_COOKIE['username']) ? $_COOKIE['username'] : null;
if ($username) {
header("location: http://a.sso1.com/index.php");
}
//跨域登陆设置cookie逻辑
if (!empty($_GET['username'])) {
//设置p3p请求头 部分浏览器支持设置
header('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"');
setcookie("username", $_GET['username'], time() + 3600);
exit($_GET['username']);
}
if (!empty($_POST['username']) && !empty($_POST['password']) &&
$_POST['password'] == "123456") {
$username = $_POST['username'];
setcookie("username", $username, time() + 3600);
//发送跨域设置cookie的请求
echo "<script src='http://a.sso2.com/login.php?username={$username}'></script>";
echo "<script> window.location.href = 'http://a.sso1.com/index.php'</script>";
exit();
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>A站点</title>
</head>
<body>
<h2>A站点</h2>
<form action="/login.php" method="post" enctype="application/x-www-form-urlencoded">
<div>
用户名:<input type="text" name="username" value="test">
</div>
<div>
密码:<input type="text" name="password" value="123456">
</div>
<div>
<button>登陆</button>
</div>
</form>
</body>
</html>
<?php
//logout.php
if (!empty($_GET['username'])) {
//设置p3p请求头 跨域清空cookie信息 达到同步退出的效果
header('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"');
setcookie("username", null, time() - 1);
exit($_GET['username']);
}else{
setcookie("username", null, time() - 1);
//发送跨域的同步退出请求
echo "<script src='http://a.sso2.com/logout.php'></script>";
echo "<script> window.location.href = 'http://a.sso1.com/index.php'</script>";
}
a.sso2.com对应目录下面的代码和以上类似 只是对域名进行修改。
该方案总结
- ucenter就是使用该方式实现单点登陆,可参考ucenter单独部署一个用户中心,提供给接入应用对应登陆退出等接口,每个应用单独对接接口,接口参数需进行加密处理。详细使用可参考ucenter源码。
- 加密函数可参考 https://www.php.net/manual/zh/book.mcrypt.php
2.3 跨域session共享(跨域SSO)
本文介绍的是一种PHP的开源SSO解决方案,可完全跨域,实现较简洁,源码地址:https://github.com/legalthings/sso
文档 :https://github.com/jasny/sso/wiki
实现原理
一共分为3个角色:
Client - 用户的浏览器
Broker - 用户访问的网站
Server - 保存用户信息和凭据的地方
每个Broker有一个ID和密码,Broker和Server事先已知道。
1.当Client第一次访问Broker时,它会创建一个随机令牌,该令牌存储在cookie中。然后Broker将Client重定向到Server,传递Broker的ID和令牌。Server使用Broker的ID、密码和令牌创建哈希,此哈希作为Key键保存当前用户会话的ID。之后Server会将Client重定向回Broker。
2.Broker可以使用令牌(来自cookie)、自己的ID和密码创建相同的哈希。在执行请求时包含此哈希。
3.Server收到请求会提取哈希,然后根据哈希获取之前保存的用户会话ID,然后将其设置成当前会话ID。因此,Broker和Client使用相同的会话。当另一个Broker加入时,它也将使用相同的会话。它们可以共享会话中保存的用户信息,进而实现了单点登录功能。
一.第一次访问系统

首次访问Broker时会进行attach操作,attach主要有以下几个动作:
1.生成token并保存到cookie当中。
2.将Broker ID和token作为URL参数跳转到Server。
3.Server根据Broker ID查询到Broker的密码,再加上传过来的token生成一个哈希,作为Key保存当前用户的浏览器与Server的会话ID。此数据需要持久保存,可指定失效时间。
最后返回最初用户访问的地址。
Broker侧attach代码片段:
public function attach($returnUrl = null)
{
if ($this->isAttached()) return;
if ($returnUrl === true) {
$protocol = !empty($_SERVER['HTTPS']) ? 'https://' : 'http://';
$returnUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
$params = ['return_url' => $returnUrl];
$url = $this->getAttachUrl($params);
header("Location: $url", true, 307);
echo "You're redirected to <a href='$url'>$url</a>";
exit();
}
server端的attach逻辑
public function attach()
{
$this->detectReturnType();
if (empty($_REQUEST['broker'])) return $this->fail("No broker specified", 400);
if (empty($_REQUEST['token'])) return $this->fail("No token specified", 400);
if (!$this->returnType) return $this->fail("No return url specified", 400);
$checksum = $this->generateAttachChecksum($_REQUEST['broker'], $_REQUEST['token']);
if (empty($_REQUEST['checksum']) || $checksum != $_REQUEST['checksum']) {
return $this->fail("Invalid checksum", 400);
}
$this->startUserSession();//启动会话
$sid = $this->generateSessionId($_REQUEST['broker'], $_REQUEST['token']);
//sid会当前的会话唯一标识 对应的值为服务端的sessionid值 进行缓存处理
//后面的请求直接可以直接拼接获取相同的sid来读取服务端的sessionId,
//重新设置session会话获取报存在session里面的数据
$this->cache->set($sid, $this->getSessionData('id'));
$this->outputAttachSuccess();//
}
二.再次访问的时候
当再次访问Broker时,由于可以从cookie中获取token,所以不会再进行attach操作了。当Broker试图获取用户信息(getUserInfo)时,会通过CURL方式和Server通信,参数中会携带哈希Key值作为Broker合法身份的验证。并且会在请求头携带attach时服务端生成的sid参数,用于在服务端获取缓存下的服务端sessionid 进行会话重置。
broker端 curl请求函数
protected function request($method, $command, $data = null)
{
if (!$this->isAttached()) {
throw new NotAttachedException('No token');
}
$url = $this->getRequestUrl($command, !$data || $method === 'POST' ? [] : $data);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
//这里需要注意 在apache环境下 Authorization 需要进行配置 服务端获取不到参数
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json', 'Authorization: Bearer '. $this->getSessionID()]);
//getSessionID 获取到的就是当前broker的唯一标识 通过此标识可在服务端获
//取服务端的sessionid
if ($method === 'POST' && !empty($data)) {
$post = is_string($data) ? $data : http_build_query($data);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
$response = curl_exec($ch);
if (curl_errno($ch) != 0) {
$message = 'Server request failed: ' . curl_error($ch);
throw new Exception($message);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
list($contentType) = explode(';', curl_getinfo($ch, CURLINFO_CONTENT_TYPE));
if ($contentType != 'application/json') {
$message = 'Expected application/json response, got ' . $contentType;
throw new Exception($message);
}
$data = json_decode($response, true);
if ($httpCode == 403) {
$this->clearToken();
throw new NotAttachedException($data['error'] ?: $response, $httpCode);
}
if ($httpCode >= 400) throw new Exception($data['error'] ?: $response, $httpCode);
return $data;
}
Server端对getUserInfo的响应片段
public function startBrokerSession()
{
if (isset($this->brokerId)) return;
//通过请求头或者其他途径获取的broker传过来的唯一标识
$sid = $this->getBrokerSessionID();
if ($sid === false) {
return $this->fail("Broker didn't send a session key", 400);
}
//获取服务端的sessionId
$linkedId = $this->cache->get($sid);
if (!$linkedId) {
return $this->fail("The broker session id isn't attached to a user session", 403);
}
if (session_status() === PHP_SESSION_ACTIVE) {
if ($linkedId !== session_id()) throw new \Exception("Session has already started", 400);
return;
}
// 重新设置会话 对sessionid
// 将当前会话的ID设置为之前保存的会话ID,然后启动会话
// 这样就可以获取之前会话中保存的数据,从而达到共享登录信息的目的
session_id($linkedId);
session_start();
$this->brokerId = $this->validateBrokerSessionId($sid);
}
public function userInfo()
{
//启动会话
$this->startBrokerSession();
$user = null;
//获取当前登陆的用户信息 在login方法里面进行设置的
$username = $this->getSessionData('sso_user');
if ($username) {
$user = $this->getUserInfo($username);
if (!$user) return $this->fail("User not found", 500); // Shouldn't happen
}
header('Content-type: application/json; charset=UTF-8');
echo json_encode($user);
}
如果用户没有登录,那么获取到的userInfo将是null,此时在Broker侧会触发登录程序,页面会跳转到登录界面,请求用户登录。用户登录的校验是在Server侧完成的,同时将用户信息保存到之前的ID的会话当中,等到下次再访问的时候就可以直接获取到用户信息了。
public function login()
{
$this->startBrokerSession();
if (empty($_POST['username'])) $this->fail("No username specified", 400);
if (empty($_POST['password'])) $this->fail("No password specified", 400);
$validation = $this->authenticate($_POST['username'], $_POST['password']);
if ($validation->failed()) {
return $this->fail($validation->getError(), 400);
}
$this->setSessionData('sso_user', $_POST['username']);
$this->userInfo();
}
/**
* Ouput user information as json.
*/
public function userInfo()
{
$this->startBrokerSession();
$user = null;
$username = $this->getSessionData('sso_user');
if ($username) {
$user = $this->getUserInfo($username);
if (!$user) return $this->fail("User not found", 500); // Shouldn't happen
}
header('Content-type: application/json; charset=UTF-8');
echo json_encode($user);
}
该方案总结
1.因为每一次broker客户端请求 都需要去服务端校验是否已经登陆,如果请求量大,可以考虑broker自行对登陆数据进行存储,但是退出时需额外同步退出程序。
2.对登陆页面直接部署在服务端,这样每个客户端不需要自行维护一套登陆逻辑代码。(已经修改,具体参考demo)
3.如果用户Broker和Server部署在同一个域名下,那么curl_exec执行之前要先关闭会话,执行之后再打开。否则在Server中无法启动一个正在使用的会话,导致长时间等待。
2.4 Cas实现单点登陆
CAS ( Central Authentication Service ) 是 Yale 大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。
cas原理
1.cas的主要结构体系: cas-server(服务端) cas-client(客户端)
CAS Server
CAS Server 负责完成对用户的认证工作 , 需要独立部署 , CAS Server 会处理用户名 / 密码等凭证(Credentials) 。
CAS Client
负责处理对客户端受保护资源的访问请求,需要对请求方进行身份认证时,重定向到 CAS Server 进行认证。(原则上,客户端应用不再接受任何的用户名密码等 Credentials )。
(1)登陆介绍

用户通过浏览器访问系统A(a.com) 系统A通过session判断该用户没有登陆系统。进行302重定向到(认证中心) www.sso.com/login?redirect=www.a.com/pageA 去访问认证中心, 认证中心一看, 没登录过, 认证中心就让用户去登录, 登录成功以后, 认证中心要做几件重要的事情 :
- 建立一个session。
- 创建一个ticket (可以认为是个随机字符串,用于客户端认证使用)
- 然后再重定向到你那里, url 中带着ticket : www.a.com/pageA?ticket=T123 与此同时cookie也会发给浏览器,比如:Set cookie : ssoid=1234, sso.com ”

当页面重定向到www.a.com/pageA?ticket=T123的时候 系统A需要接受到这个ticket然后去认证中心询问ticket是否有效。如果ticket有效,系统A需要创建系统A自己的会话系统,把用户信息报存在session里面。
这个时候如果访问系统A另外一个受保护页面PageA1,则由于系统A已经有session所以直接验证通过,不用再去认证中心验证,直接访问页面
访问系统B (b.com)

如果用户在当前浏览器继续访问系统B(www.b.cm/pageB)则系统B首先检测并未登陆,直接重定向认证中心 www.sso.com/login?redirect=www.b.com/pageB 此时由于系统A已经登陆过,所以sso.com会携带cookie认证中心自动验证通过,然后重新生成一个ticket给系统B,然后重定向到www.b.com/pageB?ticket=T5678
系统B拿着ticket去认证认证中心验证通过,生成系统B本地会话,系统B页面即可自己可以进行访问。

(2)同步退出
由于各个系统A本地都进行登陆记录,所以调用认证中心的登陆接口,
认证中心会先销毁认证中心的会话信息,然后在上面流程中,系统A和系统B分别执行了一个注册系统操作,此记录了系统的登陆数据,用户系统在退出的时候乡各个应用发送销毁本地会话的请求。
项目搭建
由于cas-server官方是java版本的 ,可自行学习搭建。我这里提供一个php版本的cas-server git地址
原理解释参考文章:
https://www.liangzl.com/get-article-detail-124454.html
https://www.cnblogs.com/x-x-736880382/p/11535616.html
本文详细介绍了PHP实现SSO单点登录的多种方法,包括同父域使用cookie共享、P3P协议跨域设置cookie以及跨域session共享。通过案例分析和代码示例,展示了每种方案的工作原理及优缺点。此外,还提到了CAS认证服务作为另一种单点登录解决方案。
529

被折叠的 条评论
为什么被折叠?



