zoukankan      html  css  js  c++  java
  • PHP 单点登录实现方案

    单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。单点登录在大型网站里使用得非常频繁,例如像阿里巴巴这样的网站,在网站的背后是成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。实现单点登录说到底就是要解决如何产生和存储那个信任,再就是其他系统如何验证这个信任的有效性,因此要点也就以下几个:

    • 存储信任
    • 验证信任

    只要解决了以上的问题,达到了开头讲得效果就可以说是SSO。最简单实现SSO的方法就是用Cookie,实现流程如下所示:

    不然发现以上的方案是把信任存储在客户端的Cookie里,这种方法虽然实现方便但立马会让人质疑两个问题:

    • Cookie不安全
    • 不能跨域免登

    对于第一个问题一般都是通过加密Cookie来处理,第二个问题是硬伤,其实这种方案的思路的就是要把这个信任关系存储在客户端,要实现这个也不一定只能用Cookie,用flash也能解决,flash的Shared Object API就提供了存储能力。

    一般说来,大型系统会采取在服务端存储信任关系的做法,实现流程如下所示:

    以上方案就是要把信任关系存储在单独的SSO系统(暂且这么称呼它)里,说起来只是简单地从客户端移到了服务端,但其中几个问题需要重点解决:

    • 如何高效存储大量临时性的信任数据
    • 如何防止信息传递过程被篡改
    • 如何让SSO系统信任登录系统和免登系统

    对于第一个问题,一般可以采用类似与memcached的分布式缓存的方案,既能提供可扩展数据量的机制,也能提供高效访问。对于第二个问题,一般采取数字签名的方法,要么通过数字证书签名,要么通过像md5的方式,这就需要SSO系统返回免登URL的时候对需验证的参数进行md5加密,并带上token一起返回,最后需免登的系统进行验证信任关系的时候,需把这个token传给SSO系统,SSO系统通过对token的验证就可以辨别信息是否被改过。对于最后一个问题,可以通过白名单来处理,说简单点只有在白名单上的系统才能请求生产信任关系,同理只有在白名单上的系统才能被免登录。

    -------------------------------------------------------------------------------------------------------

    利用webservice,session,cookie技术,来进行通用的单点登录系统的分析与设计。具体实现语言为PHP。单点 登录,英文名为Single Sign On,简称为 SSO,是目前企业,网络业务的用户综合处理的重要组成部分。而SSO的定义,是在多个应用系统中,用户只需要登陆一次就可以访问所有相互信任的应用系 统。

    动机:

    用过ucenter的全站登录方式的朋友,应该都知道这是典型的观察者模式的解决方案。用户中心作为subject, 其所属observer的注册和删除统一在ucenter的后台进行。而各个子应用站点都对应一个observer。每次用户中心的登录动作,都会触发 js脚本回调w3c标准的子站登录接口(api/uc.php)。

    这种方式的缺点,本人认为主要是两点:1. 子站点过多时,回调接口相应增多,这个在分布子站的量的限制上,如何控制来使登录效率不会太低,不好把握; 2. 当某个子站回调接口出现问题时,默认的登录过程会卡住(可以限制登录程序的执行时间,但相应出现问题子站后面的子站的回调接口就调不到了。

    基于以上问题,在实际开发过程中,本人设计了另一套单点登录系统。

    一. 登陆原理说明

    单点登录的技术实现机制:当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录;根据用户提供的登录信息,认证系统进行身份效验,如果通过效验,应该返回给用户一个认证的凭据--ticket;用户再访问别的应用的时候,就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行效验,检查ticket的合法性。如果通过效验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。

    可以看出,要实现SSO,需要以下主要的功能:

    a) 所有应用系统共享一个身份认证系统;

    b) 所有应用系统能够识别和提取ticket信息;

    c) 应用系统能够识别已经登录过的用户,能自动判断当前用户是否登录过,从而完成单点登录的功能

    基于以上基本原则,本人用php语言设计了一套单点登录系统的程序,目前已投入正式生成服务器运行。本系统程序,将ticket信息以全系统唯一的 session id作为媒介,从而获取当前在线用户的全站信息(登陆状态信息及其他需要处理的用户全站信息)。

    二. 过程说明:

    登陆流程:

    1. 第一次登陆某个站:

    a) 用户输入用户名+密码,向用户验证中心发送登录请求

    b) 当前登录站点,通过webservice请求,用户验证中心验证用户名,密码的合法性。如果验证通过,则生成ticket,用于标识当前会话的用户,并将当前登陆子站的站点标识符记录到用户中心,最后

    c) 将获取的用户数据和ticket返回给子站。如果验证不通过,则返回相应的错误状态码。

    d) 根据上一步的webservice请求返回的结果,当前子站对用户进行登陆处理:如状态码表示成功的话,则当前站点通过本站cookie保存ticket,并本站记录用户的登录状态。状态码表示失败的话,则给用户相应的登录失败提示。

    2. 登陆状态下,用户转到另一子:

    a) 通过本站cookie或session验证用户的登录状态:如验证通过,进入正常本站处理程序;否则户中心验证用户的登录状态(发送ticket到用户验证中心),如验证通过,则对返回的用户信息进行本地的登录处理,否则表明用户未登录。

    登出流程

    a) 当前登出站清除用户本站的登录状态 和 本地保存的用户全站唯一的随机id

    b) 通过webservice接口,清除全站记录的全站唯一的随机id。webservice接口会返回,登出其他已登录子站的JavaScript代码,本站输出此代码。

    c) js代码访问相应站W3C标准的登出脚本

    三. 代码说明:

    本文所涉及到相关代码,已打包上传,如有兴趣,可在本文最后下载链接处点击下载。

    1. 登陆流程:

    用户从打开浏览器开始,第一个登陆的子站点,必须调用UClientSSO::loginSSO()方法。该方法返回全站唯一的随机id用于标识该用户。该随机id在UClientSSO::loginSSO()中已通过本站cookie保存,即该子站点保留了用户已登陆标识的存根于本站。

               a) UClientSSO::loginSSO()方法如下:

    复制代码
      1 <?php
      2 /**
      3  * 用户验证中心 登陆用户处理
      4  *
      5  * @param string $username      - 用户名
      6  * @param string $password      - 用户原始密码
      7  * @param boolean $remember     - 是否永久记住登陆账号
      8  * @param boolean $alreadyEnc   - 传入的密码是否已经经过simpleEncPass加密过
      9  *
     10  * @return array   - integer $return['status'] 大于 0:返回用户 ID,表示用户登录成功
     11  *                                                -1:用户不存在,或者被删除
     12  *                                                -2:密码错
     13  *                                                                                                  -11:验证码错误
     14  *                                          string $return['username']     : 用户名
     15  *                                          string $return['password']     : 密码
     16  *                                          string $return['email']        : Email
     17  */
     18 
     19 static public function loginSSO($username, $password, $remember=false, $alreadyEnc=false) {
     20 self::_init();
     21 self::_removeLocalSid();
     22 $ret = array();
     23 
     24 //
     25 //1. 处理传入webservice接口的参数
     26 //
     27 $_params  = array(
     28                 'username' => $username,
     29                 'password' => $alreadyEnc ? trim($password) : self::simpleEncPass(trim($password)),
     30                 'ip'       => self::onlineip(),
     31                 'siteFlag' => self::$site,
     32                 'remember' => $remember
     33 );
     34 $_params['checksum'] = self::_getCheckSum($_params['username'] . $_params['password'] .
     35 $_params['ip'] . $_params['siteFlag'] . $_params['remember']);
     36 
     37 //
     38 // 2.调用webservice接口,进行登陆处理
     39 //
     40 $aRet = self::_callSoap('loginUCenter', $_params);
     41 
     42 if (intval($aRet['resultFlag']) > 0 && $aRet['sessID']) {
     43 //成功登陆
     44 //设置本地session id
     45 self::_setLocalSid($aRet['sessID']);
     46 
     47 //设置用户中心的统一session id脚本路径
     48 self::$_synloginScript = urldecode($aRet['script']);
     49 
     50 $ret = $aRet['userinfo'];
     51 } else {
     52 
     53 $ret['status'] = $aRet['resultFlag'];
     54 }
     55 
     56 return $ret;
     57 }//end of function                   [/php]
     58 
     59 b) 用户验证中心的webservice服务程序,接收到登陆验证请求后,调用UCenter::loginUCenter()方法来处理登陆请求。
     60 [php]/**
     61 * 用户验证中心 登陆用户处理
     62 *
     63 * @param string $username
     64 * @param string $password
     65 * @param string $ip
     66 * @param string $checksum
     67 * @return array
     68 */
     69 static public function loginUCenter($username, $password, $ip, $siteFlag, $remember=false) {
     70 self::_init();
     71 session_start();
     72 $ret = array();
     73 $arr_login_res     = login_user($username, $password, $ip);
     74 $res_login         = $arr_login_res['status'];                //
     75 $ret['resultFlag'] = $res_login;
     76 
     77 if ($res_login < 1) {
     78 //登陆失败
     79 } else {
     80 
     81 //登陆成功
     82 $_SESSION[self::$_ucSessKey] = $arr_login_res;
     83 
     84 $_SESSION[self::$_ucSessKey]['salt'] =
     85 self::_getUserPassSalt($_SESSION[self::$_ucSessKey]['username'], $_SESSION[self::$_ucSessKey]['password']);
     86 
     87 $ret['userinfo'] = $_SESSION[self::$_ucSessKey];
     88 $ret['sessID']   = session_id();        //生成全站的唯一session id,作为ticket全站通行
     89 
     90 //
     91 //合作中心站回调登陆接口(设置用户中心的统一session id)
     92 //
     93 self::_createCoSitesInfo();
     94 $uinfo = array();
     95 $_timestamp = time();
     96 $_rawCode = array(
     97                         'action' => 'setSid',
     98                         'sid'    => $ret['sessID'],
     99                         'time'   => $_timestamp,
    100 );
    101 if ($remember) {
    102 $uinfo = array(
    103                                 'remember' => 1,
    104                                 'username' => $username,
    105                                 'password' => $password
    106 );
    107 }
    108 
    109 $ret['script'] = '';
    110 $_rawStr = http_build_query(array_merge($_rawCode, $uinfo));
    111 
    112 //
    113 // 合作站点的全域cookie设置脚本地址
    114 //
    115 foreach ((array)self::$_coSitesInfo as $_siteInfo) {
    116 $_code = self::authcode($_rawStr, 'ENCODE', $_siteInfo['key']);
    117 $_src = $_siteInfo['url'] . '?code=' . $_code . '&time=' . $_timestamp;
    118 $ret['script'] .= urlencode('');
    119 }
    120 
    121 //
    122 // 记住已登陆战
    123 //
    124 self::registerLoggedSite($siteFlag, $ret['sessID']);
    125 
    126 unset($ret['userinfo']['salt']);
    127 }
    128 
    129 return $ret;
    130 }
    131 
    132 ?>
    复制代码

    2. 本站登陆成功后,进行本地化的用户登陆处理,其后验证用户是否登陆只在本地验证。(本地存取登陆用户状态的信息,请设置为关闭浏览器就退出)

            3. 当检测用户登陆状态时,请先调用本地的验证处理,若本地验证不通过,再调用UClientSSO::checkUserLogin()方法到用户中心检测用户的登陆状态。

    a) UClientSSO::checkUserLogin()方法如下:

    复制代码
     1 <?php
     2 /**
     3  * 用户单点登陆验证函数
     4  *
     5  * @return array   - integer $return['status'] 大于 0:返回用户 ID,表示用户登录成功
     6  *                                                                                                    0:用户没有在全站登陆
     7  *                                                -1:用户不存在,或者被删除
     8  *                                                -2:密码错
     9  *                                                -3:未进行过单点登陆处理
    10  *                                                                                                  -11:验证码错误
    11  *                                          string $return['username']     : 用户名
    12  *                                          string $return['password']     : 密码
    13  *                                          string $return['email']        : Email
    14  */
    15 public static function checkUserLogin(){
    16 self::_init();
    17 $ret = array();
    18 $_sessId = self::_getLocalSid();
    19 if (empty($_sessId)) {
    20 //永久记住账号处理
    21 if(isset($_COOKIE[_UC_USER_COOKIE_NAME]) && !empty($_COOKIE[_UC_USER_COOKIE_NAME])) {
    22 
    23 //
    24 // 根据cookie里的用户名和密码判断用户是否已经登陆。
    25 //
    26 $_userinfo = explode('|g|', self::authcode($_COOKIE[_UC_USER_COOKIE_NAME], 'DECODE', self::$_authcodeKey));
    27 
    28 $username = $_userinfo[0];
    29 $password = isset($_userinfo[1]) ? $_userinfo[1] : '';
    30 if (empty($password)) {
    31 $ret['status'] = -3;
    32 } else {
    33 return self::loginSSO($username, $password, true, true);
    34 }
    35 
    36 } else {
    37 $ret['status'] = -3;
    38 }
    39 
    40 } else {
    41 //
    42 //本站原先已经登陆过,通过保留的sesson id存根去用户中心验证
    43 //
    44 $_params  = array(
    45                         'sessId'   => $_sessId,
    46                         'siteFlag' => self::$site,
    47                         'checksum' => md5($_sessId . self::$site . self::$_mcComunicationKey)
    48 );
    49 $aRet = self::_callSoap('getOnlineUser', $_params);
    50 if (intval($aRet['resultFlag']) > 0) {
    51 //成功登陆
    52 $ret = $aRet['userinfo'];
    53 } else {
    54 $ret['status'] = $aRet['resultFlag'];
    55 }
    56 }
    57 
    58 return $ret;
    59 }                   [/php]
    60 
    61 b) 用户验证中心的webservice服务程序,接收到检验登陆的请求后,调用UCenter::getOnlineUser()方法来处理登陆请求:
    62 [php]/**
    63 * 根据sid,获取当前登陆的用户信息
    64 *
    65 * @param string $sessId        - 全站唯一session id,用做ticket
    66 * @return array
    67 */
    68 /**
    69  * 根据sid,获取当前登陆的用户信息
    70  *
    71  * @param string $sessId        - 全站唯一session id,用做ticket
    72  * @return array
    73  */
    74 static public function getOnlineUser($sessId, $siteFlag) {
    75 self::_init();
    76 session_id(trim($sessId));
    77 session_start();
    78 
    79 $ret = array();
    80 $_userinfo = $_SESSION[self::$_ucSessKey];
    81 
    82 if (isset($_userinfo['username']) && isset($_userinfo['password']) &&
    83 self::_getUserPassSalt($_userinfo['username'], $_userinfo['password'])) {
    84 $ret['resultFlag'] = "1";
    85 $ret['userinfo'] = $_userinfo;
    86 
    87 self::registerLoggedSite($siteFlag, $sessId);                //记住已登陆战
    88 unset($ret['userinfo']['salt']);
    89 } else {
    90 $ret['resultFlag'] = "0";
    91 }
    92 
    93 return ($ret);
    94 }
    95 ?>
    复制代码

    4. 单点登出时,调用UClientSSO::logoutSSO()方法。调用成功后,如需其他已登陆站立即登出,请调用 UClientSSO::getSynloginScript()方法获取W3C标准的script,在页面输出。
                 a) UClientSSO::logoutSSO()方法如下:         

    复制代码
    <?php
    /**
    * 全站单点登出
    *  - 通过webservice请求注销掉用户的全站唯一标识
    *
    * @return integer    1: 成功
    *                                     -11:验证码错误
    */
    public static function logoutSSO(){
            self::_init();
            $_sessId = self::_getLocalSid();
    
            //
            //本站没有登陆的话,不让同步登出其他站
            //
            if (empty($_sessId)) {
                    self::_initSess(true);
                    return false;
            }
            $_params  = array(
                    'sessId'   => $_sessId,
                    'siteFlag' => self::$site,
                    'checksum' => md5($_sessId . self::$site . self::$_mcComunicationKey)
            );
    
            $aRet = self::_callSoap('logoutUCenter', $_params);
            if (intval($aRet['resultFlag']) > 0) {
                    //成功登出
                    self::_removeLocalSid();                //移除本站记录的sid存根
                    self::$_synlogoutScript = urldecode($aRet['script']);
                    $ret = 1;
            } else {
                    $ret = $aRet['resultFlag'];
            }
            return intval($ret);
    }                   [/php]
    
            b) 用户验证中心的webservice服务程序,接收到全站登出请求后,调用UCenter::loginUCenter()方法来处理登陆请求:
    [php]/**
    * 登出全站处理
    *
    * @param string - 全站唯一session id,用做ticket
    * @return boolean
    */
    static public function logoutUCenter($sessId) {
            self::_init();
            session_id(trim($sessId));
            session_start();
    
            $_SESSION = array();
            return empty($_SESSION) ? true : false;
    }
    ?>
    复制代码

    四. 代码部署:         
            1. 用户验证中心设置                 
                a) 用户验证中心向分站提供的webservice服务接口文件,即UserSvc.php部署在hostname/webapps/port/ UserSvc.php中。查看wsdl内容,请访问https://hostname/port/ UserSvc.php?wsdl
                b) 用户中心用户单点服务类文件为UCenterSSO.class.php,文件路径为在hostname/webapps/include /UCenterSSO.class.php。该文件为用户单点登陆处理 的服务端类,被hostname/webapps/port/ UserSvc.php调用。用于获取用户的登陆信息,是否单点登陆的状态信息,单点登出处理等。
                c) 用户验证中心通过W3C标准,利用cookie方式记录,删除全站统一的用户唯一随机id 的脚本文件为hostname/webapps/port/cookie_mgr.php.
           
           2. 子站点设置                 
               a) 各子站点请将,UClientSSO.class.php部署在用户中心服务客户端目录下。部署好后,请修改最后一行的UClientSSO::setSite('1'); 参数值为用户验证中心统一分配给各站的标识id.
               b) 在部署的用户中心服务客户端包下的api目录下下,请将logout_sso.php脚本转移到此处,并编写进行本站登出的处理脚本。
               c) 在子站点验证用户登陆状态的代码部分,额外增加到用户中心的单点登陆验证的处理。
                   即在首先通过本站验证用户的登陆状态,如果未通过验证,则去用户中心验证。验证操作要调用UClientSSO::checkUserLogin();接口,接口含义请查看代码注释。
               d) 在分站的登出处理脚本中,通过UClientSSO::getSynlogoutScript();获取script串输出即可。

    五. 扩展功能:       
          1. 记录跟踪所有在线用户
                   因为所有用户的登录都要经过用户验证中心,所有用户的ticket都在验证中心生成,可以将用户和该ticket(session id)在内存表中建立一个映射表。得到所有在线用户的记录表。
                   后期如有必要跟踪用户状态来实现其他功能,只要跟踪这个映射表就可以了。其他功能可以为: 获取在线用户列表,判断用户在线状态,获取在线用户人数等。

     2. 特殊统计处理
                       因为整个系统登录登出要经过用户验证中心,所以可以针对用户的特殊统计进行处理。如用户每天的登录次数,登陆时间,登陆状态失效时间,各时段的在线用户人数走势等。

    六. 其他事项:       
         1. 本站登陆状态有效时间问题:                  
             全站要求用户登陆状态在关闭浏览器时就失效。要求各分站对session或cookie的处理方式按照如下进行:
              a) Session方式记录用户登陆状态的站点
                      请在站点公用脚本开始处,添加一下代码

    复制代码
    1 <?php
    2 session_write_close();
    3 ini_set('session.auto_start', 0);                    //关闭session自动启动
    4 ini_set('session.cookie_lifetime', 0);            //设置session在浏览器关闭时失效
    5 ini_set('session.gc_maxlifetime', 3600);  //session在浏览器未关闭时的持续存活时间     
    6 ?>
    复制代码

             b) cookie方式记录用户登陆状态的站点
                     请在设置用户登陆状态的cookie时,设置cookie有效时间为null.

  • 相关阅读:
    107. Binary Tree Level Order Traversal II
    103. Binary Tree Zigzag Level Order Traversal
    102. Binary Tree Level Order Traversal
    690. Employee Importance
    1723. Find Minimum Time to Finish All Jobs
    LeetCode 329 矩阵中最长增长路径
    7.2 物理内存管理
    LeetCode 面试题 特定深度节点链表
    LeetCode 100 相同的树
    npm安装包命令详解,dependencies与devDependencies实际区别
  • 原文地址:https://www.cnblogs.com/flzs/p/9955105.html
Copyright © 2011-2022 走看看