文档章节

完全跨域的单点登录(SSO)解决方案源码解析

FGQ
 FGQ
发布于 2018/10/13 20:55
字数 2051
阅读 731
收藏 14

本文介绍的是一种PHP的开源SSO解决方案,可完全跨域,实现较简洁,源码地址:https://github.com/legalthings/sso

实现原理

一共分为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加入时,它也将使用相同的会话。它们可以共享会话中保存的用户信息,进而实现了单点登录功能。

背景知识说明

Session代表着服务器和客户端一次会话的过程。直到session失效(服务端关闭),或者客户端关闭时结束。Session 是存储在服务端的,并针对每个客户端(客户),通过Session ID来区别不同用户的。关于session的详细介绍请看这篇文章。下面说的会话即指Session。

详细实现说明

以下是其GitHub中的过程图:

第一次访问流程图

首次访问Broker时会进行attach操作,attach主要有以下几个动作:

  1. 生成token并保存到cookie当中。
  2. 将Broker ID和token作为URL参数跳转到Server。
  3. Server根据Broker ID查询到Broker的密码,再加上传过来的token生成一个哈希,作为Key保存当前用户的浏览器与Server的会话ID。此数据需要持久保存,可指定失效时间。
  4. 最后返回最初用户访问的地址。

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();
    }

该解决方案的改进思考

  1. 登录界面部署在Broker中,意味着每一个Broker都要维护一套登录逻辑,可以将登录界面部署在Server端,需要登录时跳转到Server进行登录,这时需要传递登录完成之后跳转的地址。
  2. 每次获取userInfo时都要访问Server,如果访问量较大,对Server的负载能力要求比较高。可改为每个Broker只从Server端获取一次userInfo,然后将其保存到Broker的会话当中。不过这样有两点需要注意:
    • 用户注销各个Broker不会同步,如果对此要求较高,必须对各个Broker单独调用注销程序。
    • 如果用户Broker和Server部署在同一个域名下,那么curl_exec执行之前要先关闭会话,执行之后再打开。否则在Server中无法启动一个正在使用的会话,导致长时间等待。

© 著作权归作者所有

共有 人打赏支持
FGQ

FGQ

粉丝 33
博文 19
码字总数 40151
作品 0
武汉
项目经理
私信 提问
基于 Cookie 的 SSO 中间件 - kisso

kisso = cookie sso 基于 Cookie 的 SSO 中间件,它是一把快速开发 java Web 登录系统(SSO)的瑞士军刀。欢迎大家使用 kisso !! kisso 帮助文档下载 1、支持单点登录 2、支持登录Cookie缓存...

青苗
2014/06/18
0
21
单点登录(SSO)看这一篇就够了

背景 在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录,很方便。 但随着企业的发展,用到的系统随之增多,运营人员在操作...

小忽悠
2018/09/06
0
0
Flask: SSO原理及实现

现在大多数软件公司的业务不再是单条线,而是发展成多元化的产品线。包括多个网站应用、移动APP以及桌面软件,那么当然希望能实现统一用户和统一登录。统一用户基本都已实现,然而统一登录却...

陈亦
2014/02/15
0
25
基于 Web 的单点登录理论研究之跨域和票据设计

最近好多朋友问我关于 SSO 的问题,其实市面上有很多成型的产品,SSO 理论本身也提了好多年了,下面是我以前写的一篇文章《基于 Web 的单点登录理论研究》里的一部分关于跨域和票据设计问题,...

青夜之衫
2017/12/08
0
0
简单的四种方式单点登录实现(SSO)

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 在做单点登录...

王念博客
2016/04/17
3.6K
0

没有更多内容

加载失败,请刷新页面

加载更多

安装mysql8.0.11以及修改root密码、连接navicat for mysql。

安装mysql8.0.11以及修改root密码、连接navicat for mysql。   最近在学习node.js,少不得要跟数据库打交道,于是打算安装一个数据库软件,在mongedb和mysql之间选择了mysql。作为一个数据...

linjin200
2分钟前
0
0
前嗅ForeSpider教程:创建模板

今天,小编为大家带来的教程是:如何在前嗅ForeSpider中创建模板。主要内容有:模板的概念,模板的配置方式,模板的高级选项,具体内容如下: 一,模板的概念 模板列表的层级相当于网页跳转的...

forespider
4分钟前
0
0
OSChina 周三乱弹 —— 除了电脑,别人都很开心

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @小小编辑:推荐歌曲,《三亩水田》- 蚂蚁先生 《三亩水田》- 蚂蚁先生 手机党少年们想听歌,请使劲儿戳(这里) @uknow8692 :感谢失业,让我...

小小编辑
6分钟前
56
10
django数据库自动重连

简介 Django数据库连接超过wait_timeout导致连接丢失时自动重新连接数据库 https://github.com/zhanghaofei/django-db-reconnect 安装 pip install django_db_reconnect 注意仅支持pymysql...

张豪飞
10分钟前
0
0
PostMan 工具使用使用,以及不同请求对应的ContentType 的设置

https://www.jianshu.com/p/d230d27b44fe

kuchawyz
11分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部