新浪微博如何实现 SSO 的分析
最近在使用sina微博时,经常性交替使用 weibo.com 和 t.sina.cm.cn进入我的微博。发现当我在 t.sina.com.cn中登录之后,直接切换至weibo.com,这时候在 weibo.com是已经登录的,当我在 weibo.com进行注销之后,再切换至 t.sina.com.cn,这时候在 t.sina.com.cn也已经是注销的状态了。
对于SSO的实现方案及其机制,早已经不是什么新鲜的技术了,从微软为.net提供的passport机制到java中开源的JBoss SSO、Oracle OpenSSO及经典的 Yale CAS等等之类的开源或一些商业SSO中间件都不失为作为单点登录实现的选择。当然一些企业也会选择自己实现一套适合自己轻量级方案,如采用SESSIONID转递或SESSION同步复制之类的。 可以看得出SSO的价值也是具大的,就拿sina来说吧,增加 weibo.com域名之后,对于用户来说来说没有任何影响,即使你在 t.sina.com.cn中进行登录,可以无缝在两域名之间随意切换,对于它推广weibo.com无非是大大的益处。
由于近年来一直在使用 Yale的CAS作为SSO的方案,觉得 SINA的SSO与Yale-CAS有很多异曲同工之妙,于是便对SINA的SSO进行分析,其中的细节处理还是很值的学习的。当然,由于分析看到的SINA SSO处理都只是一些表现或表面上的东西,再加上其大部分关键的sso js都已经被压缩,及SERVER端的实现机制也只是靠自己的经验及结合CAS的的一些原理进行猜测。其实本文应该叫 <CAS SSO与SINA SSO的实现对比分析>更比较贴切。
好吧,进入正题。
Sina SSO之分析篇 首先是进入 t.sina.com.cn,提交用户名及密码进行登录,通过 Firebug可以看到它通过类似Aajx POST到了 http://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.3.12),如下图所示:
不难看出,其 http://login.sina.com.cn/sso/login.php 就是类似是 CAS 中的 Server,对sina的所有应用系统提供的统一登录入口。上面的参数中有一个service参数,了解 CAS的GG应该知道 cas 在登录的时候除了username 和 password同样也有一个 service 参数,其CAS该参数含义是子应用系统的服务名标识及登录成功之后所跳转的地址。当然,sina这里使用了 "miniblog"作为微博的服务名,估计他在sso-server端对 miniblog 与登录成功之后的地址进行映射,如 miniblog=http://t.sina.com.cn/,这样就避免了CAS-client中转入service= decodeURIComponent('http://t.sina.com.cn')之类的做法了。
这里的登录与CAS做法一致,将登录验证提交至统一的认证中心进行验证处理,从而避免了跨子域和全域的问题。 验证成功之后路转的路径就是service所向的地址,验证失败之后则返回至当前登录页。下面就SSO中的一些登录方面的核心问题做一些分析,看看SINA和CAS分别是如何处理的:
1.如何授权某个子系统允许其在sso-server进行登录验证呢,类似cas-server中的login-ticket;
对于cas来说,在首次进入 /cas/login页时, 会产生一个一次性的login-ticket,也就是说在提交登录验证前必须向服务器请求一个login-ticket,在登录提交时,需要将用户名及密码以及login-ticket进行提交至 cas-server端,cas-server端确定login-ticket有效后才会对用户名及密码进行认证。
看看sina如何处理的吧,继续看firebug:
以上截图是当我首次进行 t.sina.com.cn时,通过 ajax/jsonp的方式发起的一个请求,可以看到返回的callback函数中的 json 串中包含了 nonce:"SXK19N"的属性,参数名的汉译是“一次”或“一次性”的意思,估计这里的 nonce就是login-ticket,为再一次确实,我再试着提交登录看看,看它是否将该参数POST过去:
果然不出所料, nonce:"SXK19N"作为参数提交过去了,证明所猜测的应该是正确的。
2.比如验证码跨域跨服务器导致从session无法获取的问题,我们曾经遇到过;
貌似sina登录没有涉及到验证码之类的东西,当你多次登录失败之后,它采用的是“您的登录过于频繁,请稍后再试吧”,这种方案确实比验证码要好的多,而且还避免了上面的说的问题。
3. 当我登录失败了,/sso/login.php 如何将登录的错误信息返回给 t.sina.com.cn并让它进行显示呢,如果我登录成功了/sso/login.php 通过什么方式通知t.sina.com.cn呢,因为它这里使用的是ajax方式登录?
对于这方面,cas的处理是将错误信息以参数的方式返回给 client-login,如登录失败,重定向地址: http://cas-client.com?errocode=0,如果登录成功,则直接 重定向至 service 中的url,并生成ST给客户端,表示其已经在cas-server登录成功了。
看看sina如何处理的吧,随便输入一个用户名密码,提交登录,继续通过firebug看看它的处理过程:
再看看t.sina.com.cn 中的html内容的变化:
以上图1中发生了两次请求,第一次登录验证是访问 sso认证中心,它所返回response是一个html内容,第二次请求的地址: http://t.sina.com.cn/ajaxlogin.php framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack&retcode=4038&reason=%B5%C7%C2)
再结合以上图2信息,看到 html 中发生了变化,创建了一个 id=ssoLoginFrame 的iframe,于是便可以得出,sina 的登录并非原生的ajax方式,而是通过创建iframe来模拟提交不刷新的登录。也就是说,当用户点击登录提交时,这时候它会通过js创建iframe,将登录提效至该iframe中。
既然已经知道它登录是提交到iframe中,而非ajax方式,那么对于以上截图1中两个请求为什么返回的都是HTML内容就很容易解释了。再回到上面的问题,/sso/login是如何通知t.sina.com.cn登录失败了呢? 首先在以上第一个截图中返回的 HTML包含了一段 javascript:
location.replace("http://t.sina.com.cn/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack&retcode=4038&reason=%B5%C7%C2%BC%B3%A%BC%B3%A2%CA%D4%B4%CE%CA%FD%B9%FD%D3%DA%C6%B5%B7%B1%A3%AC%C7%EB%C9%D4%BA%F3%D4%D9%B5%C7%C2%BC");
<html><head><script language='javascript'> parent.sinaSSOController.feedBackUrlCallBack({"result":false,"errno":"4038","reason":"\u767b\u5f55\u5c1d\u8bd5\u6b21\u6570\u8fc7\u4e8e\u9891\u7e41\uff0c\u8bf7\u7a0d\u540e\u518d\u767b\u5f55"});</script></head><body></body></html>null
<script>try{sinaSSOController.setCrossDomainUrlList({"retcode":0,"arrURL":["http:\/\/weibo.com\/sso\/crosdom?action=login&savestate=1305607916"]});}catch(e){}try{sinaSSOController.crossDomainAction('login',function(){location.replace('http://t.sina.com.cn/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack&retcode=0');});}catch(e){}</script>
location.replace('http://t.sina.com.cn/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack&retcode=0')
<script type="text/javascript" language="javascript">location.replace("http://weibo.com/sso/login.php?url=http%3A%2F%2Fweibo.com%2F&ticket=ST-MTc4NTc0NzM0Mw==-1305008634-ja-694BA43623A3F72999AE7129A0572048&retcode=0");</script>
$.ajax({url: 'http://t.sina.com.cn/ajaxlogin.php?framelogin=0&callback=?&retcode=0', dataType:'jsonp',success:function(data){alert(data.userinfo.userid);}});
sinaSSOController.setCrossDomainUrlList({"retcode":0,"arrURL":["http:\/\/weibo.com\/sso\/crosdom?action=login&savestate=1305731311"]});}