最近在使用sina微博時(shí),經(jīng)常性交替使用 weibo.com 和 t.sina.cm.cn進(jìn)入我的微博。發(fā)現(xiàn)當(dāng)我在 t.sina.com.cn中登錄之后,直接切換至weibo.com,這時(shí)候在 weibo.com是已經(jīng)登錄的,當(dāng)我在 weibo.com進(jìn)行注銷之后,再切換至 t.sina.com.cn,這時(shí)候在 t.sina.com.cn也已經(jīng)是注銷的狀態(tài)了。
對(duì)于SSO的實(shí)現(xiàn)方案及其機(jī)制,早已經(jīng)不是什么新鮮的技術(shù)了,從微軟為.net提供的passport機(jī)制到j(luò)ava中開源的JBoss SSO、Oracle OpenSSO及經(jīng)典的 Yale CAS等等之類的開源或一些商業(yè)SSO中間件都不失為作為單點(diǎn)登錄實(shí)現(xiàn)的選擇。當(dāng)然一些企業(yè)也會(huì)選擇自己實(shí)現(xiàn)一套適合自己輕量級(jí)方案,如采用SESSIONID轉(zhuǎn)遞或SESSION同步復(fù)制之類的。 可以看得出SSO的價(jià)值也是具大的,就拿sina來說吧,增加 weibo.com域名之后,對(duì)于用戶來說來說沒有任何影響,即使你在 t.sina.com.cn中進(jìn)行登錄,可以無縫在兩域名之間隨意切換,對(duì)于它推廣weibo.com無非是大大的益處。
由于近年來一直在使用 Yale的CAS作為SSO的方案,覺得 SINA的SSO與Yale-CAS有很多異曲同工之妙,于是便對(duì)SINA的SSO進(jìn)行分析,其中的細(xì)節(jié)處理還是很值的學(xué)習(xí)的。當(dāng)然,由于分析看到的SINA SSO處理都只是一些表現(xiàn)或表面上的東西,再加上其大部分關(guān)鍵的sso js都已經(jīng)被壓縮,及SERVER端的實(shí)現(xiàn)機(jī)制也只是靠自己的經(jīng)驗(yàn)及結(jié)合CAS的的一些原理進(jìn)行猜測(cè)。其實(shí)本文應(yīng)該叫 <CAS SSO與SINA SSO的實(shí)現(xiàn)對(duì)比分析>更比較貼切。
好吧,進(jìn)入正題。
首先是進(jìn)入 t.sina.com.cn,提交用戶名及密碼進(jìn)行登錄,通過 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,對(duì)sina的所有應(yīng)用系統(tǒng)提供的統(tǒng)一登錄入口。上面的參數(shù)中有一個(gè)service參數(shù),了解 CAS的GG應(yīng)該知道 cas 在登錄的時(shí)候除了username 和 password同樣也有一個(gè) service 參數(shù),其CAS該參數(shù)含義是子應(yīng)用系統(tǒng)的服務(wù)名標(biāo)識(shí)及登錄成功之后所跳轉(zhuǎn)的地址。當(dāng)然,sina這里使用了 "miniblog"作為微博的服務(wù)名,估計(jì)他在sso-server端對(duì) miniblog 與登錄成功之后的地址進(jìn)行映射,如 miniblog=http://t.sina.com.cn/,這樣就避免了CAS-client中轉(zhuǎn)入service= decodeURIComponent('http://t.sina.com.cn')之類的做法了。
這里的登錄與CAS做法一致,將登錄驗(yàn)證提交至統(tǒng)一的認(rèn)證中心進(jìn)行驗(yàn)證處理,從而避免了跨子域和全域的問題?!◎?yàn)證成功之后路轉(zhuǎn)的路徑就是service所向的地址,驗(yàn)證失敗之后則返回至當(dāng)前登錄頁。下面就SSO中的一些登錄方面的核心問題做一些分析,看看SINA和CAS分別是如何處理的:
1.如何授權(quán)某個(gè)子系統(tǒng)允許其在sso-server進(jìn)行登錄驗(yàn)證呢,類似cas-server中的login-ticket; 對(duì)于cas來說,在首次進(jìn)入 /cas/login頁時(shí),
會(huì)產(chǎn)生一個(gè)一次性的login-ticket,也就是說在提交登錄驗(yàn)證前必須向服務(wù)器請(qǐng)求一個(gè)login-ticket,在登錄提交時(shí),需要將用戶名及密碼以及l(fā)ogin-ticket進(jìn)行提交至 cas-server端,cas-server端確定login-ticket有效后才會(huì)對(duì)用戶名及密碼進(jìn)行認(rèn)證。
看看sina如何處理的吧,繼續(xù)看firebug:
以上截圖是當(dāng)我首次進(jìn)行 t.sina.com.cn時(shí),通過 ajax/jsonp的方式發(fā)起的一個(gè)請(qǐng)求,可以看到返回的callback函數(shù)中的 json 串中包含了 nonce:"SXK19N"的屬性,參數(shù)名的漢譯是“一次”或“一次性”的意思,估計(jì)這里的 nonce就是login-ticket,為再一次確實(shí),我再試著提交登錄看看,看它是否將該參數(shù)POST過去:
果然不出所料, nonce:"SXK19N"作為參數(shù)提交過去了,證明所猜測(cè)的應(yīng)該是正確的。
2.比如驗(yàn)證碼跨域跨服務(wù)器導(dǎo)致從session無法獲取的問題,我們?cè)?jīng)遇到過; 貌似sina登錄沒有涉及到驗(yàn)證碼之類的東西,當(dāng)你多次登錄失敗之后,它采用的是“您的登錄過于頻繁,請(qǐng)稍后再試吧”,這種方案確實(shí)比驗(yàn)證碼要好的多,而且還避免了上面的說的問題。
3. 當(dāng)我登錄失敗了,/sso/login.php 如何將登錄的錯(cuò)誤信息返回給 t.sina.com.cn并讓它進(jìn)行顯示呢,如果我登錄成功了/sso/login.php 通過什么方式通知t.sina.com.cn呢,因?yàn)樗@里使用的是ajax方式登錄? 對(duì)于這方面,cas的處理是將錯(cuò)誤信息以參數(shù)的方式返回給 client-login,如登錄失敗,重定向地址: http://cas-client.com?errocode=0,如果登錄成功,則直接 重定向至 service 中的url,并生成ST給客戶端,表示其已經(jīng)在cas-server登錄成功了。
看看sina如何處理的吧,隨便輸入一個(gè)用戶名密碼,提交登錄,繼續(xù)通過firebug看看它的處理過程:
再看看t.sina.com.cn 中的html內(nèi)容的變化:
以上圖1中發(fā)生了兩次請(qǐng)求,第一次登錄驗(yàn)證是訪問 sso認(rèn)證中心,它所返回response是一個(gè)html內(nèi)容,第二次請(qǐng)求的地址: http://t.sina.com.cn/ajaxlogin.php framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack&retcode=4038&reason=%B5%C7%C2)
再結(jié)合以上圖2信息,看到 html 中發(fā)生了變化,創(chuàng)建了一個(gè) id=ssoLoginFrame 的iframe,于是便可以得出,sina 的登錄并非原生的ajax方式,而是通過創(chuàng)建iframe來模擬提交不刷新的登錄。也就是說,當(dāng)用戶點(diǎn)擊登錄提交時(shí),這時(shí)候它會(huì)通過js創(chuàng)建iframe,將登錄提效至該iframe中。
既然已經(jīng)知道它登錄是提交到iframe中,而非ajax方式,那么對(duì)于以上截圖1中兩個(gè)請(qǐng)求為什么返回的都是HTML內(nèi)容就很容易解釋了。再回到上面的問題,/sso/login是如何通知t.sina.com.cn登錄失敗了呢? 首先在以上第一個(gè)截圖中返回的 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");
location.replace的意思與location.href類似,同樣都是改變當(dāng)前的URL地址,具體區(qū)別及做法可以參考
這里及
這里。需要注意的這里所說的通過location.replace改變當(dāng)前的URL其它并非改變t.sina.com.cn的地址,而是第二個(gè)截圖里iframe中src的地址,因?yàn)檫@段HTML是在iframe中輸出的。
在 locaiton.replace 的地址中包含了一個(gè) retcode 及 reason參數(shù),估計(jì)這就是當(dāng)前登錄的錯(cuò)誤信息。在上面第一個(gè)截圖的第二個(gè)請(qǐng)求實(shí)際就是在iframe 中進(jìn)行的 location.replace操作后的跳轉(zhuǎn)地址。關(guān)鍵看它輸出的html內(nèi)容:
- <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
這段js是在 iframe中執(zhí)行的,所以可以通過 parent 進(jìn)行訪問 t.sina.com.cn中的js,可以肯定 parent.sinaSSOController.feedBackUrlCallBack 就是告訴 t.sina.com.cn 當(dāng)前已經(jīng)登錄失敗了,并且將錯(cuò)誤信息傳至該入該callback了。至此,已經(jīng)完成了 /sso/login.php 對(duì) t.sina.com.cn的信息傳送?!⌒吕斯皇怯幸皇盅剑贑AS中AJAX登錄一直都是一個(gè)問題,而sina它巧妙的通過iframe+callback 進(jìn)行實(shí)現(xiàn)了。
接著,再看看它對(duì)于登錄成功之后如何通知 t.sina.com.cn的吧,先看看登錄成功之后 sina-sso-server 會(huì)做什么,看firebug截圖:
重點(diǎn)在于 set-Cookie: tgc=TGT-MTc4NTc0NzM0Mw==-1305003116-ja-D51B2EB107B79FC50D8CA424BFE08907; 哈哈,熟悉CAS的應(yīng)該會(huì)很熟悉這個(gè),沒想到SINA的TGT與CAS的TGT不但參數(shù)命名,居然連生成的規(guī)則也一模一樣,估計(jì)sina肯定是參考了 cas 的實(shí)現(xiàn)機(jī)制。關(guān)于TGT是什么或其作用可以參考:
CAS總結(jié)之Ticket篇。另外還有一個(gè)就是當(dāng)?shù)卿洺晒χ螅瑂ina-sso-server會(huì)將用戶登陸名等等放在sina.com.cn根域的cookie中。
然后再看看登錄成功之后 sina-sso-server所返回的response內(nèi)容:
以下是從以上摘取JS部分:
- <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>
首先再次聲明,以上firebug截圖中的請(qǐng)求處理,并非 AJAX,而是在 t.sina.com.cn中放了一個(gè)iframe,輸出的 reponse都會(huì)至iframe當(dāng)中.
以上的js主要重點(diǎn)在于:
- location.replace('http://t.sina.com.cn/ajaxlogin.php?framelogin=1&callback=parent.sinaSSOController.feedBackUrlCallBack&retcode=0')
還是通過設(shè)置當(dāng)前iframe中src地址,再看看跳轉(zhuǎn)至http://t.sina.com.cn/ajaxlogin.php后的response內(nèi)容吧:
返回用戶信息(從cookie中獲取的),并且還是類似上面的做法,通過 parent.sinaSSOController.feedBackUrlCallBack回調(diào)t.sina.com.cn中的js,告訴它這個(gè)用戶已經(jīng)登錄成功了。
于是t.sina.com.cn便進(jìn)行跳轉(zhuǎn)至 t.sina.com.cn/dengers 中,從而實(shí)現(xiàn)登錄。
整體的處理流程如下:
4. 當(dāng)我在t.sina.com.cn中登錄后,切換至weibo.com,weibo.com我應(yīng)該也是已經(jīng)登錄的,如何做到呢? 對(duì)于這個(gè)問題,CAS中的處理就是,當(dāng)我進(jìn)入 weibo.com的時(shí)候,馬上跳轉(zhuǎn)至 /cas/login,然后在login中判斷cookie是否存在TGT,如果存在,并確定其有效性后,則認(rèn)為你已經(jīng)登錄,并為你生成一個(gè)ST,將ST作為ticket參數(shù)使其重定向至 weibo.com?ticket=TG-xxxx 并登錄。
看看sina怎么處理的吧,首先我直接在t.sina.com.cn登錄成功。然后再新建一個(gè)選項(xiàng)卡,輸入 weibo.com:
可以看得出,當(dāng)我進(jìn)入 weibo.com之后,sina并沒有直接進(jìn)入 weibo.com的主頁,而是馬上重定向至: http://login.sina.com.cn/sso/login.php?url=http%3A%2F%2Fweibo.com%2F&_rand=1305008634.5127&gateway=1&service=miniblog&useticket=1&returntype=META 與cas的做法確實(shí)一致?!≡倏纯丛?login.php的Response 信息,主要是JS:
- <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>
看到這里之后,不得不懷疑 SINA 的 SSO 是不是用的就是 CAS 啊?。〔坏B TGT 參數(shù)名一樣,連ST規(guī)則及參數(shù)名也一模一樣,其處理機(jī)制也十分相似。
到這里之后就與 CAS 的處理一樣了,就不詳細(xì)寫了,可以參考 CAS相關(guān)文章。
──────────
PS:由于在分析過程中里面的很多SSO關(guān)鍵JS都?jí)嚎s了,所以難免會(huì)存在誤差?!〔贿^SINA的SSO很多細(xì)節(jié)方面確實(shí)處理的很好,作為互聯(lián)網(wǎng)應(yīng)用的話,如果單純的只是把 CAS DOWNLOAD 下來,然后直接配配就用的話很多方面的處理還是很不到位的?!∮袝r(shí)間我把我們CAS參考 SINA 調(diào)整一下。
到這里,不得不說的一個(gè)事情就是,之前在分析淘寶cookie如何跨域獲取時(shí),大家都說出了一個(gè)taobao的jsonp實(shí)際存在一定的安全隱患。后面那個(gè)淘寶的GG看到之后加入Refer的判斷。而現(xiàn)在,在分析的過程中發(fā)現(xiàn)新浪也有這樣的問題,可以嘗試一下,隨便在本地建立一個(gè)html,引入jquery,然后使用下面的JS,就可以獲取到sina中的登錄郵箱名等信息,前提是你需要先在sina中登錄: - $.ajax({url: 'http://t.sina.com.cn/ajaxlogin.php?framelogin=0&callback=?&retcode=0', dataType:'jsonp',
- success:function(data){
- alert(data.userinfo.userid);
- }});