PHP单点登录的简单实现以及webserver的简单使用
时间:2014-4-28 作者:smarteng 分类: 服务器相关
说明:自己学习的一些记录和备忘,有什么问题还请指正
简单实现三个站点的单点登录,在一个站点登录,其他站点自动登录,一个站点退出,其他站点同时退出
假设有三个站点
- siteA 域名为sitea.xxx
- siteB 域名为siteb.xxx
- siteC 域名为sitec.xxx
siteC提供统一登录认证服务
要实现单点登录要满足如下的条件:
- 站点SESSION的共享,不管用户数据库是否是单独的,SESSION一定要是公共的。(这里用memcached解决共享SESSION的问题)
- 所有站点能共享一个登录凭证。(对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;
- SoapServer用于创建php服务器端页面时定义可被调用的函数,方法,对象,类及返回响应数据。
- SoapClient用于调用远程服务器上的SoapServer页面,并实现了对相应函数的调用。
- SoapFault用于生成soap访问过程中可能出现的错误。
Soap服务有两种模式:
- WSDL模式:需要手动创建一个WSDL文件.如果各个站点是不同的语言写的,比如有的用java,有的用PHP,那么用wsdl吧,接口也一目了然 {使用方法}
- 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请求的网站会比较多,影响网页的加载速度。