SSO 单点登录

老牛浏览 471评论 0发表于

1. 单系统登录机制

1.1 HTTP 无状态协议

Web 应用采用 browser/server 架构,HTTP 作为通信协议。HTTP 是无状态协议,浏览器的每一次请求,服务器都会独立处理,不会与之前或之后的请求产生关联,这个过程用下图说明,三次请求/响应对之间没有任何联系。

aac24bcf-4a50-4b26-aa8f-dec87c9fd372

这也意味着,任何用户都能通过浏览器访问服务器资源,如果想保护某些资源,必须限制浏览器请求;也就需要能够鉴别浏览器请求,响应合法请求,忽略或拒绝非法请求。要鉴别浏览器请求,必须清楚浏览器请求状态,既然 HTTP 协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制。

1.2 会话机制

浏览器第一次请求服务器,服务器创建一个会话,并将会话的 id 作为响应的一部分发送给浏览器,浏览器存储会话 id,并在后续第二次和第三次请求中带上会话 id,服务器通过请求中的会话id就知道是不是同一个用户了,如下图所示:

e58da3a6-3dbb-4bd2-ad29-c92f56e61d7c

浏览器怎么保存会话 id 呢?

使用 Session 技术。浏览器将会话 id 通过 Cookie 保存在本地,并在请求服务器的时候带上此会话 id。

e34980d5-bde5-4463-9e35-39aa3ed22dd4

1.3 登录状态

有了会话机制,登录状态就好理解了,我们假设浏览器第一次请求服务器需要输入用户名和密码验证身份,服务器拿到后去数据库比对,正确的话说明当前用户是合法用户,应该将这个会话标记为「已授权」或者「已登录」状态。既然是会话的状态,自然要保存到 Session 中。

PHP 代码如下:

php
$_SESSION['isLogin'] = true;

获取登录状态:

php
$isLogin = $_SESSION['isLogin'];
f57cb187-0b60-428b-8f64-ada89bab4cde

每次请求受保护的资源时,都会检查会话的登录状态,只有 isLogin=true 的会话才能访问,登录机制因此实现。

2. 多系统的复杂性

Web 系统已从单系统发展成为如今由多系统组成的应用群,面对如此多的系统,用户难道要一个个登录,然后一个个注销吗?

6f3ef571-4626-4a66-a5fb-0483f941ff0e

系统复杂性应该由系统内部承担,而不是用户。无论 Web 系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问整个应用群与访问单系统一样,登录/注销只要一次就够了。

248b0aa1-a25d-4328-ae82-cfdc945d775d

单系统登录解决方案的核心是 Cookie,Cookie 携带会话 id 在浏览器和服务器之间维护会话状态。但 Cookie 是有限制的,这个限制就是 Cookie 的作用域 (通常对应网站的域名),浏览器发送 HTTP 请求时会自动携带与该域匹配的 Cookie,而不是所有 Cookie。

既然这样,为什么不将 Web 应用群中所有子系统的域名统一在一个域名下,例如 *.baidu.com,然后将他们的 Cookie 域设置为 baidu.com,这种做法是可以的,甚至早期很多多系统登录就是采用这种同域共享 Cookie 的方式。

然而,可行并不代表好,共享 Cookie 的方式存在众多局限:

  • 首先,应用群域名得统一;

  • 其次,应用群各系统使用 Cookie 的 key 值(PHP 为 PHPSESSID)要相同,不然无法维持会话;

  • 第三,Cookie 本身不安全,有可能被盗取或者伪造。

因此,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录。

3. 单点登录

什么是单点登录?

单点登录全称 Single Sign On(以下简称 SSO),是指在应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录。简单来说,一次登录,多处登录;一次注销,多处注销。具体包括单点登录与单点注销两部分。

3.1 登录

SSO 需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,SSO 认证中心验证用户的用户名密码没问题,则创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以基于此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理,用下图说明:

4eff7b38-6a16-448b-9ccd-3b29aa661345

对上图简要描述:

  1. 用户访问系统 1 的受保护资源,系统 1 发现用户未登录,跳转到 SSO 认证中心,并将自己的地址作为参数传过去;

  2. SSO 认证中心发现用户未登录,将用户引导至登录界面;

  3. 用户输入用户名密码进行登录;

  4. SSO 认证中心校验用户信息,创建用户与 SSO 认证中心之间的会话(全局会话),同时创建授权令牌,并保存该令牌;

  5. SSO 认证中心带着令牌跳转回最初的请求地址(系统 1);

  6. 系统 1 拿到令牌,去 SSO 认证中心校验令牌是否有效;

  7. SSO 认证中心校验令牌,返回有效,并在 SSO 认证中心登记本次授权信息;

  8. 系统 1 使用该令牌创建与用户的会话(局部会话),返回受保护资源;

  9. 用户访问系统 2 的受保护资源;

  10. 系统 2 发现用户未登录,跳转到 SSO 认证中心,并将自己的地址作为参数传过去;

  11. SSO 认证中心发现用户已登录(用户与 SSO 认证中心的全局会话在「流程 4」中已建立),跳转回系统 2 的地址,并附带上令牌;

  12. 系统 2 拿到令牌,去 SSO 认证中心校验令牌是否有效;

  13. SSO 认证中心校验令牌,返回有效,并在 SSO 认证中心登记本次授权信息;

  14. 系统 2 使用该令牌创建与用户的局部会话,返回受保护资源。

用户登录成功后,会与 SSO 认证中心及各个子系统建立会话,用户与 SSO 认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过 SSO 认证中心,全局会话与局部会话有如下约束关系:

  • 局部会话存在,全局会话一定存在。

  • 全局会话存在,局部会话不一定存在。

  • 全局会话销毁,局部会话必须被销毁。

你可以通过博客园、百度、CSDN、淘宝等网站的登录过程加深对单点登录的理解,注意观察登录过程中的跳转 URL 与参数。

3.2 注销

单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下图来说明:

5d00ced7-5a5d-474d-86c3-ed181177584e

SSO 认证中心一直监听全局会话的状态,一旦全局会话被销毁,监听器将通知所有子系统执行注销操作。

下面是对上图的简要说明:

  1. 用户向系统 1 发起注销请求;

  2. 系统 1 根据用户与系统 1 建立的会话 id 拿到令牌,向 SSO 认证中心发起注销请求;

  3. SSO 认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌登录的系统地址;

  4. SSO 认证中心向所有登录系统发起注销请求;

  5. 各登录系统接收 SSO 认证中心的注销请求,销毁局部会话;

  6. SSO 认证中心引导用户到登录界面。

4. 部署

单点登录涉及 SSO 认证中心与众子系统,子系统与 SSO 认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须继承 SSO 的客户端,SSO 认证中心则是 SSO 服务端,整个单点登录过程实质是 SSO 客户端与服务端通信的过程。

5. 实现

只是简要介绍下基于 PHP 的实现过程,不提供完整源码,明白了原理,可以自己实现。SSO 采用客户端/服务端架构,我们先看 SSO-Client 与 SSO-Server 要实现的功能。

5.1 SSO-Client

  1. 拦截子系统未登录用户请求,跳转到 SSO 认证中心;

  2. 接收并存储 SSO 认证系统中心发出的令牌;

  3. 与 SSO-Server 通信,校验令牌的有效性;

  4. 建立局部会话;

  5. 拦截用户注销请求,向 SSO 认证中心发送注销请求;

  6. 接收 SSO 认证中心发出的注销请求,销毁局部会话。

5.2 SSO-Server

  1. 验证用户的登录信息;

  2. 创建全局会话;

  3. 创建授权令牌;

  4. 与 SSO-Client 通信发送令牌;

  5. 校验 SSO-Client 令牌有效性;

  6. 登记已登录系统;

  7. 接收 SSO-Client 注销请求,注销所有会话。

接下来,我们按照原理来一步步实现 SSO。

  1. SSO-Client 拦截未登录请求

    php
    if (isset($_SESSION['isLogin'])) {
        echo 'login success';
    } else {
        // 未登录,跳转到 SSO 认证中心
        header("Location:sso-server-url?redirect=sso-client-url");
        exit;
    }
  2. SSO-Server 拦截未登录请求
    拦截从 SSO-Client 跳转到 SSO 认证中心的未登录请求,跳转到登录界面,这个过程与 SSO-Client 完全一样。

  3. SSO-Server 验证用户登录信息
    用户在登录页面输入用户名密码,请求登录,SSO 认证中心校验用户信息,校验成功,将会话状态标记为「已登录」。

    php
    if ($isValideUser) {
        // 登录验证成功
        $_SESSION['isLogin'] = true;
    }
  4. SSO-Server 创建授权令牌
    授权令牌是一串随机字符,以什么样的方式生成都没有关系,只要不重复、不易伪造即可。

    php
    function grantToken($length = 32)
    {
        $str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $token = '';
        for ($i = 0; $i < $length; $i++) {
            $token .= $str[mt_rand(0, strlen($str) - 1)];
        }
        return $token;
    }
  5. SSO-Client 取得令牌并校验
    SSO 认证中心登录后,跳转回子系统并带上令牌,子系统取得令牌,然后去 SSO 认证中心去校验。

    php
    $token = isset($_GET['token']) ? $_GET['token'] : '';
    if (! $token) {
        $verifyToken = file_get_contents('sso-server-url?token=' . $token . '&redirect=' . 'sso-client-url');
        if ($verifyToken == 'success') {
            //有效的token
        } else {
            header('Location:sso-server-url');
            exit;
        }
    }
  6. SSO-Server 接收并处理校验令牌请求
    用户在 SSO 认证中心登录成功后,SSO-Server 创建授权令牌并存储该令牌,所以,SSO-Server 对令牌的校验就是去查找这个令牌是否存在以及是否过期,令牌校验成功之后,SSO-Server 将发送校验请求的系统登记到 SSO 认证中心。

    令牌与登录系统地址通常存储在 key-value 数据库(如 Redis 的集合)中,Redis 可以为 key 设置有效时间,也就是令牌的有效期。Redis 运行在内存中,速度非常快,正好 SSO-Server 不需要持久化任何数据。

    可能你会问,为什么要存储这些系统的地址?如果不存储,注销的时候就麻烦了,用户向 SSO 认证中心提交注销请求,SSO 认证中心注销全局会话,但不知道哪些系统用此全局会话建立了自己的局部会话,也不知道要向哪些子系统发送注销请求注销局部会话。

  7. SSO-Client 校验令牌成功创建局部会话
    令牌校验成功后,SSO-Client 将当前局部会话标记为「已登录」。

    php
    if ($verifyToken == 'success') {
        $_SESSION['$isLogin'] = true;
    }
  8. 注销过程
    用户向子系统发送带有「logout」参数的请求(注销请求),SSO-Client 拦截器拦截该请求,向 SSO 认证中心发起注销请求。

    php
    if ($action == 'logout') {
        $token = $_SESSION['token'];
        header("Location:sso-server-url?token=" . $token);
        exit;
    }

    SSO 认证中心也用同样的方式识别出 SSO-Client 的请求是注销请求(带有 logout 参数),SSO 认证中心注销全局会话。

    SSO 认证中心有一个全局会话的监视器,一旦全局会话注销,将通知所有登录系统注销。

    php
    if ($action == 'logout') {
        foreach($sso_client_url_array as $sso_client_url){
            echo "<script src='$sso_client_url?token=$token'></script>"
        };
        echo 'logout success';
    }

整个单点登录的难点在于能否理清整个流程,以及区分会话双方,避免 Session 混用。

6. 附录

  • 对于单点登录,不止这一种实现方式,但是原理基本类似;

  • 如何确保 token 验证的安全:

    • 每个子系统拥有一对 app_id 和 app_secret,校验 token 的时候,带上 app_id、token 以及和 app_secret 的签名,确保数据不被修改;

  • 如何较好的集成到现有系统,降低耦合度;

  • 理解其他授权认证方式,比如 OAuth 认证:

    • OAuth,是先在第三方应用请求用户授权,用户在认证服务器登录认证之后,第三方应用拿着 Access 去认证服务器获取用户信息,但是并没有共享 Session。也就是说下次在另一个应用登录时,仍然需要去认证服务器登录认证。而单点登录是共享 Session 的,下次访问其他应用时,会共享本次的登录状态,不需要再次登录。

点赞
收藏
暂无评论,快来发表评论吧~
私信
老牛@ilaoniu
老牛,俗称哞哞。单纯的九零后理工小青年。喜欢折腾,爱玩,爱音乐,爱游戏,爱电影,爱旅游...
最后活跃于