PHP单点登录的简单实现以及webserver的简单使用
简单实现三个站点的单点登录,在一个站点登录,其他站点自动登录,一个站点退出,其他站点同时退出
说明:功能的实现基于cookie,如果浏览器关闭了cookie的功能,将无法实现多点登陆。
假设有三个站点
- 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扩展我们主要使用三个类: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
//我也不知道这是什么,不过没有这个头,IE下,你的irame将无法写入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请求的网站会比较多,影响网页的加载速度。