發(fā)現(xiàn)我給自己設(shè)了個(gè)陷阱,對(duì)于 Acegi Security System 的解析并不是光寫(xiě)個(gè)安全認(rèn)證的流程就能說(shuō)清楚的。雖然看起來(lái) Acegi 的文檔確實(shí)挺累,但是當(dāng)我動(dòng)筆寫(xiě)時(shí)卻發(fā)現(xiàn)要寫(xiě)得比這個(gè)文檔更好還是挺難的。畢竟我們不能讓本來(lái)很難的事情一下子就變得很簡(jiǎn)單了,我也是昨天重要看了一遍了 Acegi Security System 的源代碼,才發(fā)現(xiàn)我自己有明白了好多不明白的東西。但是我仍然希望我寫(xiě)的東西能夠幫助那些正在學(xué)習(xí)和研究 Acegi Security System 的人們,所以我又開(kāi)始動(dòng)筆了,呵呵!
雖然我不想介紹太多與安全認(rèn)證流程無(wú)關(guān)的東西,但是有些東西的講解卻是必不可少的,因?yàn)闆](méi)有這些基本的概念和類(lèi),后面的東西就沒(méi)法說(shuō)清了。不過(guò)對(duì)于具體的類(lèi)、類(lèi)圖和它們之間的關(guān)聯(lián),我還是推薦去看 Professional Java Development with the Spring Framework 里關(guān)于 Acegi 的那一章,對(duì)于想讀 Acegi 的源代碼和了解 Acegi 內(nèi)部設(shè)計(jì)的人來(lái)說(shuō),這一章真是太有用了。
本來(lái)不想貼這幅類(lèi)圖的,畢竟有盜版的嫌疑,但是發(fā)現(xiàn)有些東西不貼出來(lái)又說(shuō)不清楚。整個(gè)認(rèn)證功能的核心是 Authentication 接口,我們只把 Authentication 想象成一個(gè)包含用戶(hù)基本信息的類(lèi)就行了,它里面放了用戶(hù)名、密碼、這個(gè)用戶(hù)的具體權(quán)限有哪些(當(dāng)然具體的東西是由它的子類(lèi) UsernamePasswordAuthenticationToken 實(shí)現(xiàn)的,其它類(lèi)的存放的信息稍有不同,畢竟驗(yàn)證的方式還是多種多樣的,我這里描述的所有東西 Acegi 最常用的實(shí)現(xiàn)方式,而不考慮其它的東西,否則只會(huì)分散注意力,看了之后一頭霧水)。 Authentication 里還包含了一個(gè) GrantedAuthority 接口,今天暫且不討論 Authorization 的問(wèn)題了,畢竟它與驗(yàn)證的流程是不相關(guān)的,而具體的細(xì)節(jié)又極復(fù)雜。
我們通過(guò) AuthenticationManager 這個(gè)接口來(lái)驗(yàn)證這個(gè) Authtication 對(duì)象的合法性,真正的驗(yàn)證過(guò)程看上去很懸,其實(shí)最后的實(shí)現(xiàn)無(wú)非是去數(shù)據(jù)庫(kù)搜索一下是否存在這個(gè)用戶(hù),密碼是否匹配(說(shuō)的仍然是最常用的實(shí)現(xiàn)方式,呵呵),只是它設(shè)計(jì)的時(shí)候?qū)ο蟮年P(guān)聯(lián)比較巧妙,類(lèi)圖看熟了就會(huì)覺(jué)得沒(méi)什么,真正查數(shù)據(jù)庫(kù)的那個(gè)類(lèi)是 DaoAuthenticationProvider 。這個(gè)接口真正巧妙的地方是它執(zhí)行后返回的結(jié)果是一個(gè) Authentication ,而不是用一個(gè)布爾值來(lái)表示驗(yàn)證成功或失敗。 Why? 記得當(dāng)年在 JavaEye 上有個(gè)討論 Exception 的貼子, robbin 認(rèn)為用戶(hù)安全認(rèn)證應(yīng)該用 Checked Exception 來(lái)控制流程,更多的人認(rèn)為密碼錯(cuò)誤是正常的事件流,返回布爾值更為恰當(dāng),這里不討論這兩種觀點(diǎn)的對(duì)錯(cuò),畢竟每個(gè)人站的角度不同,具體的情況也不同。
但是如果要實(shí)現(xiàn)認(rèn)證的透明性,我們要用到的卻是 unchecked exception ,這個(gè) Exception 叫做 AuthenticationException (如果是 authorization 會(huì)拋出 AccessDeniedException, 不過(guò)道理類(lèi)似),這真是奇妙的事,因?yàn)?/span> Exeception 是可以傳遞的,如果在本類(lèi)里面處理不了這個(gè) Exception ,我們就會(huì)將這個(gè) Exception 拋給調(diào)用這個(gè)類(lèi)的類(lèi),如此循環(huán),直到有一個(gè)類(lèi)可以處理它為止(對(duì)于 Web 來(lái)說(shuō)應(yīng)該是在頁(yè)面上提示登錄出錯(cuò)信息)。沒(méi)想到 Exception 的這個(gè)種特性用在安全認(rèn)證里如此的合適,權(quán)限不夠?用戶(hù)密碼不對(duì)?我才不管呢,只要拋出個(gè)異常,最后會(huì)有人把它接住處理掉的。當(dāng)然這里的另一個(gè)條件是 Unchecked ,只有 unchecked 才不會(huì)導(dǎo)致接口的污染,才能達(dá)到完全的透明性。
有了前面的基礎(chǔ)接口,我們要提出下一個(gè)問(wèn)題了,這個(gè) Authentication 對(duì)象應(yīng)該存放在哪里?幾乎每個(gè)做過(guò) Web 應(yīng)用的人告訴我: HttpSession 。 Acegi 也不例外,雖然還有其它的存放地點(diǎn)(要跟具體的 Web 容器結(jié)合,會(huì)導(dǎo)致不兼容性,一般不提倡用)。但是我們馬上會(huì)問(wèn)下一個(gè)問(wèn)題:我們?cè)趺吹玫?/span> Authentication 對(duì)象?當(dāng)然我們可以去 HttpSession 里去取,但是很多時(shí)候我們?cè)隍?yàn)證的是與 Web 層無(wú)關(guān)的(比如要用戶(hù)調(diào)用 Service 層的權(quán)限或 Domain Object 的權(quán)限)。我們必須用與 Web 無(wú)關(guān)的 API 來(lái)獲取 Authentication 。這讓我們想起了什么?對(duì),是 Webwork , Webwork 的 Action 是完全與 Web API 無(wú)關(guān)的,它的 Request 里的參數(shù)自動(dòng) populate 成了 Action 的屬性,但是我們?nèi)匀豢梢酝ㄟ^(guò) ActionContext 來(lái)獲取這些信息。它是怎么做到的?是 Threadlocal ,因?yàn)槊總€(gè) Web Request 都會(huì)使 Web 容器生成一個(gè)新的線程來(lái)處理它的這個(gè)特性使我們可以將這些 Web 的數(shù)據(jù)一股腦塞給 Threadlocal 。這個(gè)存放 Authentication 的對(duì)象叫做 SecurityContext ,而把 SecurityContext 放入 Threadloal 或取出的則是 SecurityContextHolder ,下面就是它的類(lèi)圖:
講完這些基礎(chǔ)設(shè)施,我們就可以看具體的認(rèn)證流程啦,真正的認(rèn)證是一串的 Filter (對(duì) Servlet 容器熟悉的人應(yīng)該都不要解釋了)。只不過(guò) Acegi 在這些 Filter 上稍微玩的點(diǎn)花招,因?yàn)橐话愕?/span> Filter 是不能定義在 Spring 的 ApplicationContext 里的,所以這用了一個(gè)代理的 Filter 對(duì)象 FilterToBeanProxy 將 Filter 操作 Delegate 給定義在 ApplicationContext 里的 Filter 。這個(gè)似乎跟主題無(wú)關(guān),不過(guò)如果以后真有類(lèi)似的需求的話,這倒是蠻管用的一招。當(dāng)然還有 FilterChainProxy ,它把一串的 Filter 全部定義在一個(gè) bean 里,使配置簡(jiǎn)化了好多,呵呵。
我們來(lái)看看 Filter 的一頭一尾。頭是 httpSessionContextIntegrationFilter ,其實(shí)它的功能前面已經(jīng)討論過(guò)了,在執(zhí)行前把 HttpSession 里的 Authentication 取出來(lái)放到 SessionContextHolder ( Threadlocal )里,在執(zhí)行完畢后,把 Authentication 塞回到 HttpSession 。真正的實(shí)現(xiàn)代碼有一堆,不過(guò)核心的代碼無(wú)非就這么幾行:
Object contextFromSessionObject = httpSession.getAttribute(ACEGI_SECURITY_CONTEXT_KEY);
SecurityContextHolder.setContext((SecurityContext) contextFromSessionObject);
chain.doFilter(request, response);
httpSession.setAttribute(ACEGI_SECURITY_CONTEXT_KEY, , SecurityContextHolder.getContext());
Filter 的尾是 securityEnforcementFilter ,它的工作就是進(jìn)真正的用戶(hù)認(rèn)證的流程控制了,具體的認(rèn)證工作它會(huì) delegate 給 FilterSecurityInterceptor ,但是不管怎么認(rèn)證,結(jié)果無(wú)非是認(rèn)證成功或失敗, securityEnforcementFilter 只要管抓住異常再轉(zhuǎn)到特定的頁(yè)面就行了。下面就是這個(gè)類(lèi)的信心代碼:
try {
filterSecurityInterceptor.invoke(fi);
}
} catch (AuthenticationException authentication) {
sendStartAuthentication(fi, authentication);
} catch (AccessDeniedException accessDenied) {
sendAccessDeniedError(fi, accessDenied);
}
我們?cè)賮?lái)看看用戶(hù)登錄是怎么干的吧。 Acegi 的用戶(hù)登錄很有意思,為了不讓用戶(hù)寫(xiě)任何這方面的代碼,它也直接把這個(gè)功能放到 Filter 里了,這個(gè) Filter 叫做 authenticationProcessingFilter 。這個(gè) Filter 的要求是頁(yè)面上的 form 的 Action 名字,登錄名、密碼的字段名都是定死的。一個(gè)簡(jiǎn)單的頁(yè)面就這些啦:
<form action="<c:url value=' j_acegi_security_check '/>" method="POST">
<input type='text' name='j_username'>
<input type='password' name='j_password'>
<input name="submit" type="submit">
</form>
記住 action 必須叫 j_acegi_security_check ,用戶(hù)名必須叫 j_username ,密碼必須叫 j_password 。呵呵,代碼就不寫(xiě)了,無(wú)非就是判斷只要 Action 名字對(duì),就把用戶(hù)名、密碼取出來(lái)認(rèn)證一把,最后把認(rèn)證成功的 Authetication 對(duì)象填到 SecurityContextHolder 里再導(dǎo)到相應(yīng)頁(yè)面,認(rèn)證失敗就導(dǎo)到出錯(cuò)頁(yè)面。
呵呵,好了,認(rèn)證的核心過(guò)程其實(shí)就這些了,雖然還有其它的好多的 Filter 和關(guān)聯(lián),但是當(dāng)我們把核心的內(nèi)容分析清楚之后,其它的都不難了。( Authorization 是例外,有些部分我還沒(méi)看明白)。