PHP单点登录的简单实现以及webserver的简单使用

时间:2014-4-28     作者:smarteng     分类: 服务器相关


说明:自己学习的一些记录和备忘,有什么问题还请指正

简单实现三个站点的单点登录,在一个站点登录,其他站点自动登录,一个站点退出,其他站点同时退出

假设有三个站点

  • siteA 域名为sitea.xxx
  • siteB 域名为siteb.xxx
  • siteC 域名为sitec.xxx

siteC提供统一登录认证服务

要实现单点登录要满足如下的条件:

  1. 站点SESSION的共享,不管用户数据库是否是单独的,SESSION一定要是公共的。(这里用memcached解决共享SESSION的问题)
  2. 所有站点能共享一个登录凭证。(对php程序来说 ,SESSION ID 是最简单的方法了)

所以,只要能把siteA的session id 传递给 siteB等其他站点就好办了,我们可以使用jsonp或者隐藏的iframe达到这个目的

首先在sitec上建立webserver服务端,(其他方式的登录认证API接口都可以)

  • soap_server.php

第一步,开启session,保存到memcache;

第二步,建立webserver,建议使用PHP自带的soap扩展.默认soap的扩展是没有加载的,所以你可能需要检查php.ini是否打开了

如果不知道什么是soap,{点击这里}.

soap扩展我们主要使用三个类:soapSever,soapCilent和soapFault;

  1. SoapServer用于创建php服务器端页面时定义可被调用的函数,方法,对象,类及返回响应数据。
  2. SoapClient用于调用远程服务器上的SoapServer页面,并实现了对相应函数的调用。
  3. SoapFault用于生成soap访问过程中可能出现的错误。

Soap服务有两种模式:

  1. WSDL模式:需要手动创建一个WSDL文件.如果各个站点是不同的语言写的,比如有的用java,有的用PHP,那么用wsdl吧,接口也一目了然 {使用方法}
  2. Non-WSDL模式:如果所有站点都是PHP的,这种方法简单省事,效率也要高一点。

这里使用Non-WSDL的模式

ini_set('session.save_handler', 'memcache');
ini_set('session.save_path', '127.0.0.1:11211');
session_start();//开启webserver,启动登录和认证服务require_once 'Authorize.class.php';$options = array(
    'location' => 'http://sitec.xxx/soap_server.php',
    'uri' => 'soap_server.php' );//无wsdl模式$wsdl = null; $soap = new SoapServer($wsdl, $options);$soap->setClass("Authorize");$soap->handle();
  • Authorize.class.php

你的webserver类

class Authorize {     const KEY = "a*v%^*(OH"; //ticket加密解密的密钥     /**
     * login  登录服务并返回多点登录的ticket
     * @param type $username 用户登录名或者id
     * @param type $password    登录密码
     * @param type $login_site 来源的站点(为防止加密解密失败,约定为 xxxx.xx(如:abc.xxx) 格式,自行处理)
     * @param type $ip  登录用户客户端IP
     * @return array  返回数组,error=>错误信息,'sessid'=>session id,'sso_script'=>多点登录接口脚本,ticket=>多点登录凭证
     */     public function login($username = '', $password = '', $login_site = '', $ip = '') {      
        if ($login_site == '' || $username == '' || $password == '') {
            return array();
        }
        //分配并获取session_id();         $sid = session_id();
        //返回值结构         $result = array(
            'error'=>'',  //错误信息             'sessid' => $sid, //session id             "sso_script" => 'http://sitec.xxx/sso.php?ticket=', //多点登录的脚本地址             'ticket'=>''         );

        //连接数据库,可能根据登录站点不同,连接不同的数据库         $file_path = './data.xml';
        if (!file_exists($file_path)) {
            $result['error'] = 'can not connect db';
            return $result;
        }
        $f = file_get_contents($file_path);
        $xml = new SimpleXMLElement($f);
        $users = $xml->xpath("/users/user[name='{$username}']");
        $user = $users[0];
        if ($user) {
            $uname = (string) $user->name;
            $upassword = (string) $user->password;
            if ($password != $upassword) {
                $result['error'] =  'username or password is wrong !';
                return $result;
            }
            $uid = (string) $user->attributes()->id;
            $_SESSION['uid'] = $uid;
            $_SESSION['uname'] = $uname;
            $_SESSION['upassword'] = $upassword;
            $_SESSION['islogin'] = 'true';
            $_SESSION['login_site'] = $login_site;
            $_SESSION['ip'] = $ip;
            $_SESSION['ticket_expire'] = 1;  //ticket登录计数器,获取需要登录的站点个数,每登录一个-1;<=0将不能使用             //生成ticket             require_once 'encrypt.php';
            $key = $ip . self::KEY . $login_site; //用ip和登录地址做密钥防止接口连接被复制粘贴;如果session周期太长,仍然是不安全的             $ticket = encrypt($result['sessid'], "E", $key);
            $result['ticket'] = $ticket;
            $result['sso_srcipt'] .= $ticket;
            return $result;
        }
    }
    /**
     * decryptTicket ticket解密并验证有效性
     * @param type $ticket
     * @param type $server_name
     * @return type array("sid")
     */     public function decryptTicket($ticket = '', $login_site = '', $ip = '') {         //返回值结构         $res = array(
            "sid" => '',
            "error" => ''         ); 
        if ($ticket == '' || $login_site == '') {
            $res['error'] = "tciket和login_site不能为空";
            return $res;
        }
        //解密ticket,获得session id后返回         $key = $ip . self::KEY . $login_site;
        require_once 'encrypt.php';
        $sid = encrypt($ticket, "D", $key);
        if($sid){
           $res['sid'] = $sid;
        }
        return $res;
    }

    /**
     * checkTickerExpire 检查ticket是否有效
     * @return int 1为认证成功,0为失败
     */     public function checkTickerExpire() {         if ($_SESSION['ticket_expire'] && ($_SESSION['ticket_expire'] -- > 0)) {
            return 1;
        }
        return 0;
    }

}
  • encrypt.php

任意加密解密字符串的函数或者类库都可以

  • sso.php

sso_script,这个是在其他各个站点登录成功之后加载的脚本。

<?php/*
 * 远程登录jsonp脚本,这个脚本放在每个网站需要登录的页面,也可以在登录成功时用js动态加载
 * 各个站点从配置获取
 * 确保能获得用户IP和当前登录的站点域名,两者可能会被用来作为钥匙解密ticket
 *///ini_set('session.save_handler', 'memcache');//ini_set('session.save_path', '127.0.0.1:11211');//session_start();//如果要在siteC上也登录,就传递过来ticket进行认证或者把siteC也加入到到sites数组中, header("Content-type:text/javascript;charset=utf-8");

$sites = array('http://sitea.xxx', 'http://siteb.xxx');
$refer_info = pathinfo($_SERVER["HTTP_REFERER"]);
$refer = $refer_info['dirname'];

$ticket = '';if (is_string($_GET['ticket'])) {
    $ticket = $_GET['ticket'];
}//irame框架 $iframes = '';
foreach ($sites as $site) {
    if($refer == $site){
        continue;
    }//    $iframes .= ' <iframe style="display:none" width=0 height=0 frameborder=0  src="' . $api . '/login_api.php"></iframe> ';
    $iframes .= '<iframe   src="' . $site . '/login_api.php"></iframe> ';
}

$js = <<<JSCODE            window.onload = function(){
                var ticket = "{$ticket}"
                if(!ticket){
                    //从cookie从获取ticket                     var ticket_arr = document.cookie.split(";").filter(
                        function(e){
                            var cookie = e.split("=")
                            return cookie[0].replace(" ","") === "ticket"
                         }
                    )
                    if(ticket_arr[0]){
                        ticket = ticket_arr[0].split("=")[1]
                    }else{
                        //ticket不存在
                        return
                    }   
                }

                //创建隐藏的ifrme,调用认证接口
                div = document.createElement("div")
                div.innerHTML = '{$iframes}'               
                var iframes = div.childNodes                 var len = iframes.length                 for (var i= 0;i<len;i++){
                    new_src = iframes[i].getAttribute("src")+'?ticket='+ticket
                    iframes[i].setAttribute('src',new_src)
                }
                document.body.appendChild(div)
                //销毁ticket,防止反复登录
                var date = new Date();
                date.setTime(date.getTime() - 10000);
                document.cookie = "ticket" + "=a; expires=" + date.toGMTString();
            }
JSCODE;
echo $js;

站点的登录认证,session的同步

  • index.php webserver登录认证

这里仅仅是演示soapColent的使用,就像调用本地对象方法一样。

function do_login() {     $username = is_string($_REQUEST['username']) ? $_REQUEST['username'] : '';
    $password = is_string($_REQUEST['password']) ? $_REQUEST['password'] : '';

    if ($username == '' || $password == '') {
        echo 'bad username or password !';
        return;
    }
    //确保这里给的site_name要和sso_script提供的login_site一致,否则会导致认证失败     $site_name = $_SERVER['SERVER_NAME'];
    $ip = $_SERVER["REMOTE_ADDR"];
    //开启sopa客户端,无wsdl模式     $wsdl = null;
    $options = array(
        'location' => 'http://sitec.xxx/soap_server.php',
        'uri' => 'soap_server.php'     );
    $soap = new SoapClient($wsdl, $options);
    $result = $soap->login($username, $password, $site_name, $ip);
    if ($result['error']) {
        echo $result['error'];
    } else {
        setcookie('PHPSESSID', $result['sessid']);
        //如果是跳转到某个页面,将ticket保存到cookie,手动添加sso_script脚本         //如果是ajax可以直接返回 $result['sso_sript'];登录成功后加载这个jsonp即可         setcookie('ticket', $result['ticket']);
        header('location:index.php?act=show_index');
    }
}//销毁session或者标识用户状态,达到单点退出的目的function do_logout() {     //session_unset();      //session_destroy();     $_SESSION['islogin'] = 'false';
    header('location:index.php?act=show_login');
}
  • 手动在HTML页面加上单点登录的js本,或者使用jsonp请求ajax登录成功后返回的脚本连接
function show_index() {
    if (empty($_SESSION['islogin']) || $_SESSION['islogin'] == 'false') {
        header('location:index.php?act=show_login');
    }
    echo '<html><head><script type="text/javascript" src="http://sitec.xxx/sso.php"></script></head><body>';
    $sid = session_id();
    echo "welcome {$_SESSION['uname']},your id is {$_SESSION['uid']},password is {$_SESSION['upassword']},sessionid is {$sid}";
    echo " ,ip is {$_SESSION['ip']} , from {$_SESSION['login_site']}";
    echo '<a href="?act=do_logout">[logout]</a>';

    echo '</body></html>';
}
  • login_api.php 在每个站点的目录下配置一个登入脚本

这个脚本接收传递过来的ticket,然后连接websever认证,获取session_id ;将session_id 写入cookie

//P3P协议,针对IE浏览器的跨域写COOKIE header('P3P: CP="CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI DSP COR"');
ini_set('session.save_handler', 'memcache');
ini_set('session.save_path', '127.0.0.1:11211');
session_start();$site = $_SERVER['SERVER_NAME'];if ($_SESSION['islogin'] === 'true') {
    echo $site, '重复登录,直接返回';
    exit(0);
}if ($_GET['ticket'] && $_SERVER['SERVER_NAME']) {
    $ip = $_SERVER["REMOTE_ADDR"];
    $refer_info = parse_url($_SERVER["HTTP_REFERER"]);
    $login_site = $refer_info['host'];
    $ticket = $_GET['ticket'];
    //认证ticket     $soap = new SoapClient(null, array("location" => "http://sitec.xxx/soap_server.php", "uri" => "soap_server.php"));
    $res = $soap->decryptTicket($ticket, $login_site, $ip);
    if (!$res['error'] && $res['sid']) {
        $sid = $res['sid'];
        $soap->__setCookie("PHPSESSID", $sid);
        if ($soap->checkTickerExpire()) {
            setcookie("PHPSESSID", $sid);
            echo $site, '登录成功';
        }
    } else {
        echo $site, "登录失败", $res['error'];
    }
}

总结
* 实现方法主要是通过iframe或者跨域把session id写入cookie
* 方法的实现依赖与cookie的跨域写入和session的多站共享
* 优点是js后台执行,不影响登录速度
* 缺点是站点多了的话iframe请求的网站会比较多,影响网页的加载速度。