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