有四个网站分别为:
www.sso.com/login.php
www.sso.com/logout.php
www.a.com/sso.php
b和c站的文件跟a站是一样的,只需复制a站即可,这里就不重复了。
user表结构如下:
www.a.com www.b.com www.c.com www.sso.com
需求是如果我们在sso登陆后,其他网站也会显示登陆中,不需要重复登陆,退出时,其他网站也会失效。
解决流程如下:
1、我们需要统一这四个站的session存储方式。 (session共享,保存到mysql中)
2、登陆和退出的请求统一由sso这个站处理。(其他站全部跳转到sso上)
3、当用户在sso.com登陆时,我们在验证完用户名和密码正确后,通过获取session_id和生成一段skey加密串保存到user表的相应字段中。然后跳转到一个中间页面,访页面通过iframe的方式引入a.com,b.com,和c.com。
<iframe src="http://www.a.com/sso.php?sessid=xxx&skey=xxx" width="0" height="0" frameborder="0"> </iframe> <iframe src="http://www.b.com/sso.php?sessid=xxx&skey=xxx" width="0" height="0" frameborder="0"> </iframe> <iframe src="http://www.c.com/sso.php?sessid=xxx&skey=xxx" width="0" height="0" frameborder="0"> </iframe>
(*传给a,b,c这三个站的sessid是经过加密的,为了安全。)
4、a,b,c这三个站的sso.php拿到sessid和skey解密后得到session_id,然后到user表中进行比对,如果正确,设置当前会话的session_id,然后session_start(),因为四个站的session是共享的,所以a,b ,c三个站就拿到了在sso上登陆时存储的会话数据。
//sessid是sso.com上产生的session_id session_id(sessid); session_start();
具体的代码如下:
www.sso.com/index.php
<?php header('Content-Type:text/html;charset=utf-8'); require_once './DBSession.php'; session_start(); if(array_key_exists('userInfo', $_SESSION) && !empty($_SESSION['userInfo'])) { $userInfo = $_SESSION['userInfo']; echo '欢迎', $userInfo['name'], '<br />'; echo '<a href="./logout.php">退出</a>'; } else { header('Location: ./login.html'); }
www.sso.com/login.html
<!DOCTYPE HTML> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title></title> <style> * {padding:0;margin:0;} .loginForm {450px;height:auto;margin:100px auto 0 auto;} .loginForm table {100%;border-collapse:collapse;} .loginForm table td, .loginForm table th {border:1px solid #dcdcdc;padding:5px;} </style> </head> <body> <form action="./login.php" method="post" class="loginForm"> <table> <tr> <th colspan="2">用户登陆</th> </tr> <tr> <td>用户名:</td> <td><input type="text" name="uname" value="" /></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="upwd" value="" /></td> </tr> <tr> <td> </td> <td><input type="submit" name="submit" value="登陆" /></td> </tr> </table> </form> </body> </html>
<?php require_once './DBSession.php'; require_once './DB.php'; session_start(); if(!empty($_POST)) { $uname = !empty($_POST['uname']) ? trim($_POST['uname']) : ''; $upwd = !empty($_POST['upwd']) ? trim($_POST['upwd']) : ''; $upwd = md5($upwd); $ret = mysql_query("select `id`,`name` from `user` where `name`='{$uname}' and `pwd`='{$upwd}';", $db); $data = mysql_fetch_assoc($ret); if($data) { $userInfo = array( 'id' => $data['id'], 'name' => $data['name'], ); $_SESSION['userInfo'] = $userInfo; //通过用户ID,用户名和时间戳的md5生成skey $skey = md5($data['id'] . $data['name'] . time()); $sessId = session_id(); $userId = $data['id']; $ret = mysql_query("update `user` set `sessid`='$sessId', `skey`='$skey' where `id`={$userId};", $db); if(mysql_affected_rows($db)) { //取skey前三个字符和后三个字符生成key $key = substr($skey, 0, 3) . substr($skey, -1, 3); $lifeTime = ini_get('session.gc_maxlifetime'); $sessId = authcode($sessId, 'ENCODE', $key, $lifeTime); $appSSOUrl = array( 'http://www.a.com/sso.php', 'http://www.b.com/sso.php', 'http://www.c.com/sso.php', ); require 'iframe.php'; } } else { header('Location: ./login.html'); } }
www.sso.com/iframe.php
<!DOCTYPE HTML> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <?php foreach($appSSOUrl as $item) { ?> <iframe src="<?php echo $item . '?sessid=' . $sessId . '&skey=' . $skey;?>" width="0" height="0" frameborder="0"></iframe> <?php } ?> </body> <script type="text/javascript"> window.onload = function() { location.href = 'index.php'; }; </script> </html>
<?php require_once './DBSession.php'; require_once './DB.php'; session_start(); if(array_key_exists('userInfo', $_SESSION) && !empty($_SESSION['userInfo'])) { $uid = $_SESSION['userInfo']['id']; $ret = mysql_query("update `user` set `sessid`='', `skey`='' where id={$uid}", $db); if(session_destroy()) { header('Location: ./login.html'); } }
www.a.com/index.php
<?php header('Content-Type:text/html;charset=utf-8'); require_once './DBSession.php'; session_start(); if(array_key_exists('userInfo', $_SESSION) && !empty($_SESSION['userInfo'])) { $userInfo = $_SESSION['userInfo']; echo '欢迎', $userInfo['name'], '<br />'; echo '<a href="http://www.sso.com/logout.php">退出</a>'; } else { header('Location: http://www.sso.com/login.html'); }
<?php require_once './DBSession.php'; require_once './DB.php'; $sessid = !empty($_GET['sessid']) ? trim($_GET['sessid']) : ''; $skey = !empty($_GET['skey']) ? trim($_GET['skey']) : ''; if(empty($sessid) || empty($skey)) { exit; } $key = substr($skey, 0, 3) . substr($skey, -1, 3); $sessid = authcode($sessid, 'DECODE', $key); if(!empty($sessid)) { $ret = mysql_query("select count(*) as num from user where sessid='{$sessid}' and skey='{$skey}'"); $row = mysql_fetch_assoc($ret); if($row['num']) { session_id($sessid); session_start(); } } else { header('Location: http://www.sso.com/login.html'); }
DBSession.php代码:
<?php class DBSession { protected $db = null; protected $lifeTime = 0; protected $sessTable = ''; public function __construct($db, $sessTable) { $this->db = $db; $this->sessTable = $sessTable; $this->lifeTime = ini_get('session.gc_maxlifetime'); ini_set('session.save_handler', 'user'); session_set_save_handler( array($this, "open"), array($this, "close"), array($this, "read"), array($this, "write"), array($this, "destroy"), array($this, "gc") ); register_shutdown_function('session_write_close'); } public function open($savePath, $sessName) { return true; } public function close() { $this->gc($this->lifeTime); return true; } public function read($sessId) { $time = time(); $ret = mysql_query("SELECT `data` FROM `{$this->sessTable}` WHERE `sid`='{$sessId}' AND `expire` > {$time};", $this->db); if($ret) { $row = mysql_fetch_assoc($ret); return $row['data']; } return ''; } public function write($sessId, $sessData) { $expire = time() + $this->lifeTime; $sessData = mysql_real_escape_string($sessData); $ret = mysql_query("SELECT COUNT(*) AS cnt FROM `{$this->sessTable}` WHERE `sid`='{$sessId}';", $this->db); $row = mysql_fetch_assoc($ret); if($row['cnt']) { $sql = "UPDATE `{$this->sessTable}` SET `data`='{$sessData}', `expire`={$expire} WHERE `sid`='{$sessId}';"; } else { $sql = "INSERT INTO `{$this->sessTable}` (`sid`,`expire`,`data`) VALUES('{$sessId}',{$expire},'{$sessData}');"; } mysql_query($sql, $this->db); if(mysql_affected_rows($this->db)) { return true; } return false; } public function destroy($sessId) { mysql_query("DELETE FROM `{$this->sessTable}` WHERE `sid`='{$sessId}';", $this->db); if(mysql_affected_rows($this->db)) { return true; } return false; } public function gc($lifeTime) { $time = time(); mysql_query("DELETE FROM `{$this->sessTable}` WHERE `expire` < {$time};", $this->db); return mysql_affected_rows($this->db); } } $sessDb = mysql_connect('127.0.0.1', 'root', '') or die('connect error'); mysql_select_db('test', $sessDb) or ('select db error'); mysql_query('set names utf8', $sessDb); new DBSession($sessDb, 'session');
session表结构如下:
CREATE TABLE `session` ( `sid` varchar(32) NOT NULL DEFAULT '' COMMENT 'session_id', `expire` int(11) NOT NULL COMMENT '过期时间', `data` text COMMENT 'session数据', PRIMARY KEY (`sid`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='session表';
DB.php文件代码如下:
<?php $db = mysql_connect('127.0.0.1', 'root', '') or die('connect error'); mysql_select_db('test', $db) or ('select db error'); mysql_query('set names utf8', $db);
CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID', `name` varchar(32) NOT NULL DEFAULT '' COMMENT '用户名', `pwd` varchar(32) NOT NULL DEFAULT '' COMMENT '用户密码(md5)', `sessid` varchar(32) NOT NULL DEFAULT '' COMMENT 'session_id(未加密)', `skey` varchar(32) NOT NULL DEFAULT '' COMMENT '安全码', PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';
加密解密函数authcode(取自Discuz)代码如下:
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0){ if($operation == 'DECODE') { $string = str_replace('[a]','+',$string); $string = str_replace('[b]','&',$string); $string = str_replace('[c]','/',$string); } $ckey_length = 4; $key = md5($key ? $key : 'livcmsencryption'); $keya = md5(substr($key, 0, 16)); $keyb = md5(substr($key, 16, 16)); $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : ''; $cryptkey = $keya.md5($keya.$keyc); $key_length = strlen($cryptkey); $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; $string_length = strlen($string); $result = ''; $box = range(0, 255); $rndkey = array(); for($i = 0; $i <= 255; $i++) { $rndkey[$i] = ord($cryptkey[$i % $key_length]); } for($j = $i = 0; $i < 256; $i++) { $j = ($j + $box[$i] + $rndkey[$i]) % 256; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; } for($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); } if($operation == 'DECODE') { if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) { return substr($result, 26); } else { return ''; } } else { $ustr = $keyc.str_replace('=', '', base64_encode($result)); $ustr = str_replace('+','[a]',$ustr); $ustr = str_replace('&','[b]',$ustr); $ustr = str_replace('/','[c]',$ustr); return $ustr; } }
我们通过在sso.com上登陆
登成功后会跳转到index.php
然后我们访问a.com,b.com ,c.com查看
我们点击任一网站退出,其他网站都已退出。
原理其实很简单,我们统一了四个站的session存储方式,然后让a,b,c三个站拿到用户在sso上登陆的会话id。这样会话信息就共享了。不过实现的很粗糙,仅供学习使用。
参考资料:
http://www.zhihu.com/question/19779937