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