完全跨域的单点登录(SSO)解决方案源码解析
本文介绍的是一种PHP的开源SSO解决方案,可完全跨域,实现较简洁,源码地址:https://github.com/legalthings/sso
实现原理
一共分为3个角色:
-
Client - 用户的浏览器
-
Broker - 用户访问的网站
-
Server - 保存用户信息和凭据的地方
每个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加入时,它也将使用相同的会话。它们可以共享会话中保存的用户信息,进而实现了单点登录功能。
背景知识说明
Session代表着服务器和客户端一次会话的过程。直到session失效(服务端关闭),或者客户端关闭时结束。Session 是存储在服务端的,并针对每个客户端(客户),通过Session ID来区别不同用户的。关于session的详细介绍请看这篇文章。下面说的会话即指Session。
详细实现说明
以下是其GitHub中的过程图:
首次访问Broker时会进行attach操作,attach主要有以下几个动作:
- 生成token并保存到cookie当中。
- 将Broker ID和token作为URL参数跳转到Server。
- Server根据Broker ID查询到Broker的密码,再加上传过来的token生成一个哈希,作为Key保存当前用户的浏览器与Server的会话ID。此数据需要持久保存,可指定失效时间。
- 最后返回最初用户访问的地址。
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(); }
该解决方案的改进思考
- 登录界面部署在Broker中,意味着每一个Broker都要维护一套登录逻辑,可以将登录界面部署在Server端,需要登录时跳转到Server进行登录,这时需要传递登录完成之后跳转的地址。
- 每次获取userInfo时都要访问Server,如果访问量较大,对Server的负载能力要求比较高。可改为每个Broker只从Server端获取一次userInfo,然后将其保存到Broker的会话当中。不过这样有两点需要注意:
- 用户注销各个Broker不会同步,如果对此要求较高,必须对各个Broker单独调用注销程序。
- 如果用户Broker和Server部署在同一个域名下,那么curl_exec执行之前要先关闭会话,执行之后再打开。否则在Server中无法启动一个正在使用的会话,导致长时间等待。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
如何排查java应用中CPU使用率高或内存占用高的问题
如何排查java应用中CPU使用率高或内存占用高的问题?这类问题的排查步骤基本通用的。现在通过一个具体的例子来说明。 问题描述 最近有个线上项目每天0点过后CPU使用率会上升至200%到300%。 排查过程 本节内容是对排查过程的复盘,过程记录会比较详细。如果想知道具体的命令操作,可以直接看总结部分内容。 1)当CPU再次暴涨的时候,首先我们可以通过top -c查看CPU使用率高的进程的PID。 2)然后使用top -p PID -H查看CPU使用率高的线程信息。如果CPU使用率高的线程是比较固定的,那么我们记下对应线程的PID。 执行top -p 14639 -H得出下图结果: 记下4个线程的PID: 14643、14644、14641、14642 3)接下来通过jstack PID > xxx.log输出java应用当前堆栈信息到文件。 4)第2步中,我们记下了CPU使用率高的线程PID,现在将4个线程的PID转成16进制: 3933、3934、3931、3932。接着在jstack输出的堆栈文件里,搜索nid等于3933、3934、3931、3932的线程信息。如下图: 从...
- 下一篇
Node.js股票模拟交易后台
我曾经花了一周时间开发了一个股票模拟交易后台程序,使用Node.js。代码量很少,能完成基本功能。下面给大家介绍一下其实现步骤。 基本功能 开户 搜索股票 挂单(多单、空单) 撤单(主动、被动) 成交(非撮合) 除权、除息 查询 订单状态 持仓 今日委托 今日成交 历史委托 历史成交 挂单列表 账户详情(总收益,收益率,总资产) 其中模拟交易和真实交易最大的不同是,真实交易采用撮合制,逻辑较为复杂。模拟交易采用更简单的即时成交机制,只要符合条件,订单立即成交。 这个后台程序一共就两个js文件,一个用于处理成交,即判断成交条件,写数据库。另一个处理其他逻辑。当然这里面没有提到获取股票实时价格的问题,这是另一个系统完成,我们通过消息队列实时获取我们所关心的股票的价格,这是另一个话题了。 这个后台程序以一个node.js进程的方式运行,一个10秒一次的定时器执行成交判断。(真实交易所的撮合器也是10秒钟一次) 此外有一个WebAPI Server接受来自客户端的请求。所以总体架构,可以看成是一个微服务组成的系统。 数据库设计 账户表 `Id` int(11) NOT NULL AUTO_IN...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- 2048小游戏-低调大师作品
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池