多个站点单点登录的设计思路
一般来说单点认证都需要两端来完成,在认证中心端的我们称之为SSO,在网站端的模块我们称之为PSO。两个模块之间采用二次重定向技术来实现同步两端票据的方式来实现单点登陆。
为什么需要单点登录
产品刚上线时,一般由于用户量少,所有的功能都放在一起,一般也不需要具体的单点登录。随着用户量和业务发展的需要,要求逐步将产品按功能或性能分为相应独立的站点,并分开部署,这就需要在各个站点之间进行单点登录,以达到用户一次登录,就可以使用多个站点。
单点登录的实现
简单方法: 在同一个域内的站点,可以简单的通过共享Cookie(将登录用户名存放Cookie中)来实现单点登录。这种方法实现简单,安全性方法可以通过将Cookie值加密方式加强,但对不同域下及不同开发语言下(如A站点C#,B站点C)实现麻烦。
推荐方法:建立统一的认证中心,认证中心提供:
?用户登录认证(认证用户名和密码),如果成功,返回表示本次登录的登录Token
?登录Token认证(认证Token是否正确),如果成功,返回当前登录的用户名
?延长Token有效期
?退出(使Token失效)
认证中心独立于各个站点,单点流程一般场景如下:
?用户在站点A输入用户名和密码点击登录
?站点A将用户名和密码转发给认证中心进行认证,认证中心返回Token
?站点A将当前登录用户和Token存入Session(或Cookie)
?在站点A上点击连接访问站点B,通过URL参数方式,将Token带给站点B
?站点B将Token转交到认证中心,认证正确,返回当前用户名。
?站点B将当前登录用户和Token存入Session(或Cookie),完成登录流程
这样的设计下的Token,还可以用在异步使用Ajax去访问服务器端的接口(接口可能独立部署在不同的站点下),这样只需要带上Token,服务器端的接口认证Token通过后,直接返回这个登录用户的相应用户信息。
安全性考虑
?用户登录认证接口,可增加认证频率、认证IP等限制,防止暴力攻击。
?Token其实就是一串表示本次登录的唯一字符串,可以生成字符串时,增加摘要信息。如Token的组成为:A+MD5(A+PWD) 的方式,A为随机生成的GUID,这样在验证Token时,就可以直接通过算法来验证合法性,只有算法验证通过后,再进行下一步的操作。
登陆后产生一个SSO的票据,这个票据是最重要的,因为它是决定用户是否登陆的关键。这个票据可以是Cookie,也可以是Session,我比较倾向于Cookie,因为现在有3DES加密,加密后篡改Cookie几乎成为不可能,所以无论是对于服务器负担来说还是安全性都是Cookie比较好,可能人认为万一不支持Cookie呢,不过我想Demo应该没问题吧,大不了我设计成两个都支持。
PS,为什么不用非对称加密?其实那个效率不高,3DES的安全性已经足够了,至少现在还没有人宣称能破解。
在登陆后就可以通知用户你已经登陆了,现在你可以去访问成员站点了,这个时候用户点击了成员站点的URL,进去了,这个时候首先就需要接受PSO组件的盘查,你有没有PSO的票据呢?很显然是没有的,所以这个时候请求就被Redirect回了认证中心,认证中心检查用户已经有了SSO的票据了,认为用户已经登录了,就把用户的SSO票据附加在URL后边然后Redirect回成员站点,成员站点的PSO这个时候获取到了SSO票据,于是知道了用户已经在认证端登录了,于是就创建一个PSO票据,然后返回给用户他所请求的内容。所以我们来看看其实PSO的逻辑更加复杂一点。
我们可以看到其实两个模块的功能都不算复杂,这里存在几个现实的问题,第一个是加密问题,票据需要加密,传输的URL也需要加密。
在SSO把票据通过URL发送给PSO的时候,如果我们能够截获这个URL,不管他加没有加密,在下一次我们直接用这个URL去访问站点的时候因为已经包含SSO票据了,所以PSO会认为已经登陆了而直接产生PSO票据然后就让用户进去了,这显然是一个漏洞。所以呢,我们需要在这里给这个URL加一点盐值(所谓盐值其实就是加点料),我们通过在URL里加入时间戳来让这个URL具备时间限制,这个样子URL具备失效期,过了这个时间即使截获到了这个URL也完全没有作用了。
上面的可能写的比较乱,看下面的:
一、什么是单点登录
解释:登录一个系统后,其它系统无需再次登录,即可进入。
二、举个例子:
你登录了淘宝,然后你进入天猫,发现你不用登录了。这时你要注意到,淘宝跟天猫可是完全不一样的域名。
你登录淘宝后,你的浏览器得到了cookie,但是这个cookie是在淘宝域名下的。你的天猫域名下并没有cookie。
这个时候你要想办法让天猫域名下也有这个相同的cookie。假设你的天猫域名下也有这个cookie。
当你的浏览器进入天猫时,你的浏览器会携带天猫域名下的cookie去服务器验证。
但是这个cookie对应的可是淘宝域名下的session数据,这个时候又该如何呢。
从这个例子中引出两个问题:
1、跨域种cookie
2、服务端保持cookie对应的session数据是一样的
三、解决思路
四、示例代码
A域名下的 index.php 文件
<?php header("Content-type: text/html; charset=utf-8"); if(!empty($_POST)) { $response = curl_post('http://www.U.com/index.php',$_POST); if($response['code'] > 0) { die('error:'.$response['msg']); } set_cookie('SID',$response['data']['sid'],0,'/','',0,1); echo '登录成功'; $url = 'http://www.B.com/setcookie.php?sid='.$response['data']['sid']; die('<script type="text/javascript" src="'.$url.'" reload="1"></script>'); } function curl_post( $url , $arrPost = array() , $func = "http_build_query" ){ $ch = curl_init(); $opt[CURLOPT_URL] = $url; $opt[CURLOPT_RETURNTRANSFER] = 1; $opt[CURLOPT_TIMEOUT] = 10; if( !empty( $arrPost ) ){ if( $func == 'json_encode' ) $opt[CURLOPT_HTTPHEADER] = array("Content-Type: application/json;charset=UTF-8"); $opt[CURLOPT_POST] = 1; $opt[CURLOPT_POSTFIELDS] = call_user_func( $func , $arrPost ); } curl_setopt_array ( $ch, $opt ); $response = curl_exec( $ch ); curl_close( $ch ); return json_decode($response, true); } /** * 免刷新写入cookie * name 必需。规定 cookie 的名称。 * value 必需。规定 cookie 的值。 * expire 必需。规定 cookie 的有效期。 * path 可选。规定 cookie 的服务器路径。 * domain 可选。规定 cookie 的域名。 * secure 可选。规定是否通过安全的 HTTPS 连接来传输 cookie。 * httponly 可选。规定是否禁止js读取cookie。 */ function set_cookie($name,$value='',$expire=0,$path='/',$domain='',$secure=0,$httponly=0) { $_COOKIE[$name] = $value; if(is_array($value)){ foreach($value as $k=>$v){ if(is_array($v)){ foreach($v as $a=>$b){ setcookie($name.'['.$k.']['.$a.']',$b,$expire,$path,$domain,$secure,$httponly); } }else{ setcookie($name.'['.$k.']',$v,$expire,$path,$domain,$secure,$httponly); } } }else{ setcookie($name,$value,$expire,$path,$domain,$secure,$httponly); } } ?> <!DOCTYPE HTML> <html> <head> <title>A域名的登录框</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <form action="http://www.A.com/index.php" method="post"> <input type="text" name="uname" placeholder="帐号"/> <input type="password" name="upasswd" placeholder="密码"/> <input type="submit" value="提交" /> </form> </body> </html>
B域名下的 index.php 文件
<?php header("Content-type: text/html; charset=utf-8"); $sid = isset($_COOKIE['SID']) ? $_COOKIE['SID'] : ''; if(empty($sid)) echo "请先登录"; $response = curl_post('http://www.U.com/index.php',array('sid'=>$sid)); if($response['code'] > 0) { die('error:'.$response['msg']); } die('您的帐号密码是:'.$response['data']['uinfo']); function curl_post( $url , $arrPost = array() , $func = "http_build_query" ){ $ch = curl_init(); $opt[CURLOPT_URL] = $url; $opt[CURLOPT_RETURNTRANSFER] = 1; $opt[CURLOPT_TIMEOUT] = 10; if( !empty( $arrPost ) ){ if( $func == 'json_encode' ) $opt[CURLOPT_HTTPHEADER] = array("Content-Type: application/json;charset=UTF-8"); $opt[CURLOPT_POST] = 1; $opt[CURLOPT_POSTFIELDS] = call_user_func( $func , $arrPost ); } curl_setopt_array ( $ch, $opt ); $response = curl_exec( $ch ); curl_close( $ch ); return json_decode($response, true); }
B域名下的 setcookie.php 文件
<?php header("Content-type: text/html; charset=utf-8"); $sid = isset($_GET['sid']) ? $_GET['sid'] : ''; if(!empty($sid)) { set_cookie('SID',$sid,0,'/','',0,1); } /** * 免刷新写入cookie * name 必需。规定 cookie 的名称。 * value 必需。规定 cookie 的值。 * expire 必需。规定 cookie 的有效期。 * path 可选。规定 cookie 的服务器路径。 * domain 可选。规定 cookie 的域名。 * secure 可选。规定是否通过安全的 HTTPS 连接来传输 cookie。 * httponly 可选。规定是否禁止js读取cookie。 */ function set_cookie($name,$value='',$expire=0,$path='/',$domain='',$secure=0,$httponly=0) { $_COOKIE[$name] = $value; if(is_array($value)){ foreach($value as $k=>$v){ if(is_array($v)){ foreach($v as $a=>$b){ setcookie($name.'['.$k.']['.$a.']',$b,$expire,$path,$domain,$secure,$httponly); } }else{ setcookie($name.'['.$k.']',$v,$expire,$path,$domain,$secure,$httponly); } } }else{ setcookie($name,$value,$expire,$path,$domain,$secure,$httponly); } }
U域名下的 index.php 文件
<?php header("Content-type: text/html; charset=utf-8"); $sid = isset($_POST['sid']) ? $_POST['sid'] : ''; if(!empty($sid)) { $uname_upasswd = file_get_contents($sid); if(empty($uname_upasswd)) { die(json_encode(array( 'code'=>1, 'msg'=>'fail', 'data'=>array() ))); } die(json_encode(array( 'code'=>0, 'msg'=>'success', 'data'=>array('uinfo'=>$uname_upasswd) ))); } $uname = isset($_POST['uname']) ? $_POST['uname'] : ''; $upasswd = isset($_POST['upasswd']) ? $_POST['upasswd'] : ''; if($uname == '' || $upasswd == '') { die(json_encode(array( 'code'=>1, 'msg'=>'fail', 'data'=>array() ))); } define('SID_SALT', '密码盐'); $passwd = passwd($uname.$upasswd,SID_SALT); file_put_contents($passwd, $uname.','.$upasswd); die(json_encode(array( 'code'=>0, 'msg'=>'success', 'data'=>array('sid'=>$passwd) ))); function passwd($string,$salt) { return md5(substr(md5($string).md5($salt),16,48)); }
验证流程:
1、进入A域名,输入帐号密码点击登录。
2、进入B域名,此时会打印出你在A域名输入的帐号密码。
注意:这只是一个简单的验证,实际开发中需要做cookie加密,实效验证等。而且,其实单点登录问题,还有其他解决思路。