1、为什么要做SSO?
在猎豹移动游戏开放平台刚开始的时候,我们的首要需求是实现OAuth2协议来为CP提供接入功能。
但随着我们的项目在发展,论坛、客服、用户中心也进行开发以及展望,不同的系统之间,帐号需要互通,实现单点登录,因此SSO应运而生。
也许有人说OAuth2也能实现单点登录,为什么不直接所有的系统都通过OAuth协议来实现统一登录。
对于大型的平台,SSO单点登录是必须的。OAuth给用户资源的授权提供了一个安全的、开放而又简易的标准,能够更安全、更方便的给第三方提供某些用户授权的信息。但和OAuth不同的是,对于我们自己的系统来说,不需要进行授权就能让用户进行使用。
一个很好的例子就是腾讯的QQ登录功能,对于第三方例如京东,就是使用OAuth协议进行授权,而对于腾讯微博、QQ空间,则是通过SSO来实现单点登录。
2、如何实现SSO?
SSO有以下几种方式实现:
-
共享Cookie,这种是我们最先采取的方式。
当我们的子系统都在一个父级域名下时,我们可以将Cookie种在父域下,这样浏览器同域名下的Cookie则可以共享,这样可以通过Cookie加解密的算法获取用户SessionID,从而实现SSO。
但是,后面我们发现这种方式有几种弊端:
a. 所有同域名的系统都能获取SessionID,易被修改且不安全;
b. 跨域无法使用。
所以到后面抛弃这种做法。 -
Ticket验证,我们目前采取的是这种方式。
这种实现的SSO有以下几个步骤:
a. 用户访问某个子系统,发现如果未登录,则引导用户跳转到SSO登录页面;
b. 判断SSO是否已经登录;
c. 如果已经登录,直接跳转到回调地址,并返回认证ticket;
d. 如果未登录,用户正确输入用户名/密码,认证通过跳转到回调地址,并返回认证ticket;
e. 子系统获取ticket,调用SSO获取用户uid等信息,成功后让用户登录。
3、SSO实现机制
以下是我们当前SSO的时序图
SSO系统生成ticket并跳转
$ticket = $this->generate_ticket($appid, 60, $redis); if(strpos($ret['data'],"?")) { $ret['data'] .= "&ticket=$ticket"; } else { $ret['data'] .= "?ticket=$ticket"; } $ret['data'] .= ($request->get('state')) ? "&state=".$request->get('state') : ""; function generate_ticket($appid, $timeout, $redis) { $uuid = \J20\Uuid\Uuid::v4(false);; $ticket = md5($uuid.self::SALT); $data = array('sid' => session_id(), 'appid' => $appid); $redis->setex($ticket, 60, json_encode($data)); return $ticket; }
子系统换票
$ip = $config->redis->ip; $port = $config->redis->port; $redis = new \Redis(); $redis->connect($ip, $port); $ticket = $request->get('ticket'); $resp = array('code' => \ecode\Ecode::OK); if($redis->exists($ticket)) { $data = json_decode($redis->get($ticket), TRUE); $redis->delete($ticket); session_destroy(); session_id($data['sid']); session_start(); $resp['uid'] = $_SESSION['uid']; if(isset($_SESSION['ptoken'])) { $resp['ptoken'] = $_SESSION['ptoken']; } else { $resp['ptoken'] = $_SESSION['ptoken'] = \account\Tools::str_random() } $ptlogout = \account\Tools::str_random(); $resp['ptlogout'] = $ptlogout; $info = array('ptlogout' => $ptlogout, 'sid' => $request->get('sid')); $_SESSION['ptlogin'][$data['appid']] = $info; } else { $resp['code'] = \ecode\Ecode::SSOTicketInvalid; } $resp['msg'] = constant("L::ecode_".$resp['code']); return \account\Tools::json_ret($resp);
4、如何实现统一退出
当子系统换票拿ticket去SSO获取用户信息的时候,会获取到2个参数:ptoken/ptlogout。
那么这2个参数有什么用呢?
1. 当A子系统退出的时候,如果需要通知SSO退出,就需要用到ptoken。 2. A子系统引导用户访问SSO退出的接口,并带上ptoken作为参数。 3. 当SSO验证参数ptoken与本地存储一致时,从而信任调用它的子系统,主动退出。
4. 这时候已登录过的B子系统要怎么退出呢?ptlogout这时候就发挥它的作用了。SSO拿ptlogout去调用B子系统的退出接口,ptlogout双方一致时,B子系统退出。
$ptoken = $request->get('ptoken'); $appid = $request->get('appid'); $map = explode(',', $config->apps->map); $apps = array(); foreach($map as $item) { $i = explode('-', $item); $apps[$i[0]] = $i[1]; } if(array_key_exists($appid, $apps) && $ptoken == $_SESSION['ptoken']) { // 已登录过的子系统 $ptlogin = json_decode($_SESSION['ptlogin'], TRUE); foreach($ptlogin as $k => $v) { // 判断是否是当前调用系统,如果是则跳过本次循环,如果不是则调用退出接口 if($appid == $k) continue; $base_url = $config->$apps[$k]->logout; $base_url .= (strpos($base_url, '?')) ? '&' : '?'; $url = $base_url.'?ticket='.$v['ptlogout'].'&sid='.$v['sid']; $ret = file_get_contents($url); } } session_destroy();
5、SSO的表现形式 — iframe
使用iframe的好处就是对于自己内部接入SSO的系统来说,不用关心用户是如何登录的。
1. 当需要使用SSO登录时,只需在页面嵌入iframe。
2. 当iframe中SSO已存在登录状态的时候,可以直接实现无缝的跳转,若没有登录状态,则显示登录框提供用户登录的功能,认证通过后再跳转。
3. 这样对于子系统,不需要写太多的代码就能实现单点登录。
4. 为了适应不同子系统的需求,我们还能通过参数配置,来显示不同的样式,来控制不同的跳转返回。
效果图: