本文介绍的是一种PHP的开源SSO解决方案,可彻底跨域,实现较简洁,源码地址:github.com/legalthings…php
一共分为3个角色:git
Client - 用户的浏览器github
Broker - 用户访问的网站数据库
Server - 保存用户信息和凭据的地方json
每一个Broker有一个ID和密码,Broker和Server事先已知道。跨域
当Client第一次访问Broker时,它会建立一个随机令牌,该令牌存储在cookie中。而后Broker将Client重定向到Server,传递Broker的ID和令牌。Server使用Broker的ID、密码和令牌建立哈希,此哈希做为Key键保存当前用户会话的ID。以后Server会将Client重定向回Broker。浏览器
Broker可使用令牌(来自cookie)、本身的ID和密码建立相同的哈希。在执行请求时包含此哈希。服务器
Server收到请求会提取哈希,而后根据哈希获取以前保存的用户会话ID,而后将其设置成当前会话ID。所以,Broker和Client使用相同的会话。当另外一个Broker加入时,它也将使用相同的会话。它们能够共享会话中保存的用户信息,进而实现了单点登陆功能。cookie
Session表明着服务器和客户端一次会话的过程。直到session失效(服务端关闭),或者客户端关闭时结束。Session 是存储在服务端的,并针对每一个客户端(客户),经过Session ID来区别不一样用户的。关于session的详细介绍请看这篇文章。下面说的会话即指Session。session
如下是其GitHub中的过程图:
首次访问Broker时会进行attach操做,attach主要有如下几个动做:
Broker侧attach代码片断:
/** * Attach our session to the user's session on the SSO server. * * @param string|true $returnUrl The URL the client should be returned to after attaching */
public function attach($returnUrl = null) {
/* 经过检测Cookie中是否有token来判断是否已attach 若已经attach,就再也不进行attach操做了 */
if ($this->isAttached()) return;
/* 将当前访问的地址做为返回地址,attach结束以后会返回到returnUrl */
if ($returnUrl === true) {
$protocol = !empty($_SERVER['HTTPS']) ? 'https://' : 'http://';
$returnUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
$params = ['return_url' => $returnUrl];
/* 在getAttachUrl函数中会生成token并保存到cookie中, 同时将Broker ID和token做为url的参数传递给Server */
$url = $this->getAttachUrl($params);
/* 跳转到SSO Server并退出 */
header("Location: $url", true, 307);
echo "You're redirected to <a href='$url'>$url</a>";
exit();
}
复制代码
Server侧attach代码片断:
/** * Attach a user session to a broker session */
public function attach() {
/* 检测返回类型 */
$this->detectReturnType();
/* 检测attach的url上是否带有Broker ID和token信息 */
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);
/* 根据Broker ID对应的密码和token生成校验码,与请求参数中的校验码匹配,若是相同则认为 attach的Broker是已在SSO Server注册过的 */
$checksum = $this->generateAttachChecksum($_REQUEST['broker'], $_REQUEST['token']);
if (empty($_REQUEST['checksum']) || $checksum != $_REQUEST['checksum']) {
return $this->fail("Invalid checksum", 400);
}
/* 开启session */
$this->startUserSession();
/* 根据Broker ID对应的密码和token生成哈希sid */
$sid = $this->generateSessionId($_REQUEST['broker'], $_REQUEST['token']);
/* 将哈希sid做为键值保存session id到cache中,cache具备持久保存能力,文本文件或数据库都可 */
$this->cache->set($sid, $this->getSessionData('id'));
/* 根据返回类型返回 */
$this->outputAttachSuccess();
}
复制代码
当再次访问Broker时,因为能够从cookie中获取token,因此不会再进行attach操做了。当Broker试图获取用户信息(getUserInfo
)时,会经过CURL方式和Server通讯,参数中会携带哈希Key值做为Broker合法身份的验证。
/** * Execute on SSO server. * * @param string $method HTTP method: 'GET', 'POST', 'DELETE' * @param string $command Command * @param array|string $data Query or post parameters * @return array|object */
protected function request($method, $command, $data = null) {
/* 判断是否已attach */
if (!$this->isAttached()) {
throw new NotAttachedException('No token');
}
/* 获取SSO Server地址 */
$url = $this->getRequestUrl($command, !$data || $method === 'POST' ? [] : $data);
/* 初始化CURL并设置参数 */
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
/* 添加哈希Key值做为身份验证 */
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json', 'Authorization: Bearer '. $this->getSessionID()]);
if ($method === 'POST' && !empty($data)) {
$post = is_string($data) ? $data : http_build_query($data);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
/* 执行CURL并获取返回值 */
$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);
}
/* 对返回值按照json格式解析 */
$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
的响应片断:
/** * Start the session for broker requests to the SSO server */
public function startBrokerSession() {
/* 判断Broker ID是否已设置 */
if (isset($this->brokerId)) return;
/* 从CURL的参数中获取哈希Key值sid */
$sid = $this->getBrokerSessionID();
if ($sid === false) {
return $this->fail("Broker didn't send a session key", 400);
}
/* 尝试从cache中经过哈希Key值获取保存的会话ID */
$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;
}
/******** 下面这句代码是整个SSO登陆实现的核心 ******** * 将当前会话的ID设置为以前保存的会话ID,而后启动会话 * 这样就能够获取以前会话中保存的数据,从而达到共享登陆信息的目的 * */
session_id($linkedId);
session_start();
/* 验证CURL的参数中获取哈希Key值sid,获得Broker ID */
$this->brokerId = $this->validateBrokerSessionId($sid);
}
/** * Ouput user information as json. */
public function userInfo() {
/* 启动以前保存的ID的会话 */
$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
}
/* 响应CURL,返回用户信息 */
header('Content-type: application/json; charset=UTF-8');
echo json_encode($user);
}
复制代码
若是用户没有登陆,那么获取到的userInfo将是null,此时在Broker侧会触发登陆程序,页面会跳转到登陆界面,请求用户登陆。用户登陆的校验是在Server侧完成的,同时将用户信息保存到以前的ID的会话当中,等到下次再访问的时候就能够直接获取到用户信息了。
/** * Authenticate */
public function login() {
/* 启动以前保存的ID的会话 */
$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();
}
复制代码