Spring Security3對CAS的支援主要在這個spring-security-cas-client-3.0.2.RELEASE.jar包中
Spring Security和CAS內建的配置資料很多。這裡講解的比較詳細
http://lengyun3566.iteye.com/blog/1358323
配置方面,主要為下面的部分:
<code><</code><code>security:http</code> <code>auto-config</code><code>=</code><code>"true"</code> <code>entry-point-ref</code><code>=</code><code>"casAuthEntryPoint"</code> <code>access-denied-page</code><code>=</code><code>"/error/403.jsp"</code><code>> </code>
<code> </code><code><</code><code>security:custom-filter</code> <code>ref</code><code>=</code><code>"casAuthenticationFilter"</code> <code>position</code><code>=</code><code>"CAS_FILTER"</code><code>/> </code>
<code> </code><code><</code><code>security:form-login</code> <code>login-page</code><code>=</code><code>"/login.jsp"</code><code>/> </code>
<code> </code><code><</code><code>security:logout</code> <code>logout-success-url</code><code>=</code><code>"/login.jsp"</code><code>/> </code>
<code> </code><code><</code><code>security:intercept-url</code> <code>pattern</code><code>=</code><code>"/admin.jsp*"</code> <code>access</code><code>=</code><code>"ROLE_ADMIN"</code><code>/> </code>
<code> </code><code><</code><code>security:intercept-url</code> <code>pattern</code><code>=</code><code>"/index.jsp*"</code> <code>access</code><code>=</code><code>"ROLE_USER,ROLE_ADMIN"</code><code>/> </code>
<code> </code><code><</code><code>security:intercept-url</code> <code>pattern</code><code>=</code><code>"/home.jsp*"</code> <code>access</code><code>=</code><code>"ROLE_USER,ROLE_ADMIN"</code><code>/> </code>
<code> </code><code><</code><code>security:intercept-url</code> <code>pattern</code><code>=</code><code>"/**"</code> <code>access</code><code>=</code><code>"ROLE_USER,ROLE_ADMIN"</code><code>/> </code>
<code></</code><code>security:http</code><code>> </code>
<code><</code><code>security:authentication-manager</code> <code>alias</code><code>=</code><code>"authenticationmanager"</code><code>> </code>
<code> </code><code><</code><code>security:authentication-provider</code> <code>ref</code><code>=</code><code>"casAuthenticationProvider"</code><code>/> </code>
<code></</code><code>security:authentication-manager</code><code>> </code>
<code> </code>
<code><</code><code>bean</code> <code>id</code><code>=</code><code>"casAuthenticationProvider"</code> <code>class</code><code>=</code><code>"org.springframework.security.cas.authentication.CasAuthenticationProvider"</code><code>> </code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"ticketValidator"</code> <code>ref</code><code>=</code><code>"casTicketValidator"</code><code>/> </code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"serviceProperties"</code> <code>ref</code><code>=</code><code>"casService"</code><code>/> </code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"key"</code> <code>value</code><code>=</code><code>"docms"</code><code>/> </code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"authenticationUserDetailsService"</code> <code>ref</code><code>=</code><code>"authenticationUserDetailsService"</code><code>/> </code>
<code></</code><code>bean</code><code>> </code>
<code><</code><code>bean</code> <code>id</code><code>=</code><code>"casAuthEntryPoint"</code> <code>class</code><code>=</code><code>"org.springframework.security.cas.web.CasAuthenticationEntryPoint"</code><code>> </code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"loginUrl"</code> <code>value</code><code>=</code><code>"https://server:8443/cas/login"</code><code>/> </code>
<code></</code><code>bean</code><code>> </code>
<code><</code><code>bean</code> <code>id</code><code>=</code><code>"casService"</code> <code>class</code><code>=</code><code>"org.springframework.security.cas.ServiceProperties"</code><code>> </code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"service"</code> <code>value</code><code>=</code><code>"http://localhost:8888/docms/j_spring_cas_security_check"</code><code>/> </code>
<code><</code><code>bean</code> <code>id</code><code>=</code><code>"casAuthenticationFilter"</code> <code>class</code><code>=</code><code>"org.springframework.security.cas.web.CasAuthenticationFilter"</code><code>> </code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"authenticationManager"</code> <code>ref</code><code>=</code><code>"authenticationmanager"</code><code>/> </code>
<code><</code><code>bean</code> <code>id</code><code>=</code><code>"casTicketValidator"</code> <code>class</code><code>=</code><code>"org.jasig.cas.client.validation.Cas20ServiceTicketValidator"</code><code>> </code>
<code> </code><code><</code><code>constructor-arg</code> <code>value</code><code>=</code><code>"https://server:8443/cas/"</code><code>/> </code>
<code><</code><code>bean</code> <code>id</code><code>=</code><code>"authenticationUserDetailsService"</code> <code>class</code><code>=</code><code>"org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"</code><code>> </code>
<code> </code><code><</code><code>property</code> <code>name</code><code>=</code><code>"userDetailsService"</code> <code>ref</code><code>=</code><code>"userDetailsManager"</code><code>/> </code>
<code></</code><code>bean</code><code>></code>
這裡需要強調一下http标簽的entry-point-ref屬性,因為之前沒有着重的介紹,英文的意思是入口點引用。為什麼需要這個入口點呢。這個入口點其實僅僅是被ExceptionTranslationFilter引用的。前面已經介紹過ExceptionTranslationFilter過濾器的作用是異常翻譯,在出現認證異常、通路異常時,通過入口點決定redirect、forward的操作。比如現在是form-login的認證方式,如果沒有通過UsernamePasswordAuthenticationFilter的認證就直接通路某個被保護的url,那麼經過ExceptionTranslationFilter過濾器處理後,先捕獲到通路拒絕異常,并把跳轉動作交給入口點來處理。form-login的對應入口點類為LoginUrlAuthenticationEntryPoint,這個入口點類的commence方法會redirect或forward到指定的url(form-login标簽的login-page屬性)
清楚了entry-point-ref屬性的意義。那麼與CAS內建時,如果通路一個受保護的url,就通過CAS認證對應的入口點org.springframework.security.cas.web.CasAuthenticationEntryPoint類redirect到loginUrl屬性所配置的url中,即一般為CAS的認證頁面(比如:https://server:8443/cas/login)。
下面為CasAuthenticationEntryPoint類的commence方法。其主要任務就是構造跳轉的url,再執行redirect動作。根據上面的配置,實際上跳轉的url為:
<a href="https://server:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8888%2Fdocms%2Fj_spring_cas_security_check" target="_blank">https://server:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8888%2Fdocms%2Fj_spring_cas_security_check</a>
<code>public</code> <code>final</code> <code>void</code> <code>commence(</code><code>final</code> <code>HttpServletRequest servletRequest, </code><code>final</code> <code>HttpServletResponse response, </code>
<code> </code><code>final</code> <code>AuthenticationException authenticationException) </code><code>throws</code> <code>IOException, ServletException { </code>
<code> </code><code>final</code> <code>String urlEncodedService = createServiceUrl(servletRequest, response); </code>
<code> </code><code>final</code> <code>String redirectUrl = createRedirectUrl(urlEncodedService); </code>
<code> </code><code>preCommence(servletRequest, response); </code>
<code> </code><code>response.sendRedirect(redirectUrl); </code>
<code>}</code>
接下來繼續分析custom-filter ref="casAuthenticationFilter" position="CAS_FILTER"
這是一個自定義标簽,并且在過濾器鍊中的位置是CAS_FILTER。這個過濾器在何時會起作用呢?帶着這個疑問繼續閱讀源碼
CasAuthenticationFilter對應的類路徑是
org.springframework.security.cas.web.CasAuthenticationFilter
這個類與UsernamePasswordAuthenticationFilter一樣,都繼承于AbstractAuthenticationProcessingFilter。實際上所有認證過濾器都繼承這個抽象類,其過濾器本身隻要實作attemptAuthentication方法即可。
CasAuthenticationFilter的構造方法直接向父類的構造方法傳入/j_spring_cas_security_check用于判斷目前請求的url是否需要進一步的認證處理
<code>public</code> <code>CasAuthenticationFilter() { </code>
<code> </code><code>super</code><code>(</code><code>"/j_spring_cas_security_check"</code><code>); </code>
CasAuthenticationFilter類的attemptAuthentication方法源碼如下
<code>public</code> <code>Authentication attemptAuthentication(</code><code>final</code> <code>HttpServletRequest request, </code><code>final</code> <code>HttpServletResponse response) </code>
<code> </code><code>throws</code> <code>AuthenticationException { </code>
<code> </code><code>//設定使用者名為有狀态辨別符 </code>
<code> </code><code>final</code> <code>String username = CAS_STATEFUL_IDENTIFIER; </code>
<code> </code><code>//擷取CAS認證成功後傳回的ticket </code>
<code> </code><code>String password = request.getParameter(</code><code>this</code><code>.artifactParameter); </code>
<code> </code><code>if</code> <code>(password == </code><code>null</code><code>) { </code>
<code> </code><code>password = </code><code>""</code><code>; </code>
<code> </code><code>} </code>
<code> </code><code>//構造UsernamePasswordAuthenticationToken對象 </code>
<code> </code><code>final</code> <code>UsernamePasswordAuthenticationToken authRequest = </code><code>new</code> <code>UsernamePasswordAuthenticationToken(username, password); </code>
<code> </code><code>authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); </code>
<code> </code><code>//由認證管理器完成認證工作 </code>
<code> </code><code>return</code> <code>this</code><code>.getAuthenticationManager().authenticate(authRequest); </code>
在之前的源碼分析中,已經詳細分析了認證管理器AuthenticationManager認證的整個過程,這裡就不再贅述了。
由于AuthenticationManager是依賴于具體的AuthenticationProvider的,是以接下來看
<code><</code><code>security:authentication-provider</code> <code>ref</code><code>=</code><code>"casAuthenticationProvider"</code><code>/> </code>
<code></</code><code>security:authentication-manager</code><code>></code>
意這裡的ref屬性定義。如果沒有使用CAS認證,此處一般定義user-service-ref屬性。這兩個屬性的差別在于
ref:直接将ref依賴的bean注入到AuthenticationProvider的providers集合中
user-service-ref:定義DaoAuthenticationProvider的bean注入到AuthenticationProvider的providers集合中,并且DaoAuthenticationProvider的變量userDetailsService由user-service-ref依賴的bean注入。
由此可見,采用CAS認證時,AuthenticationProvider隻有AnonymousAuthenticationProvider和CasAuthenticationProvider
繼續分析CasAuthenticationProvider是如何完成認證工作的
<code>Java代碼 </code>
<code>public</code> <code>Authentication authenticate(Authentication authentication) </code><code>throws</code> <code>AuthenticationException { </code>
<code> </code><code>//省略若幹判斷 </code>
<code> </code><code>CasAuthenticationToken result = </code><code>null</code><code>; </code>
<code> </code><code>//注意這裡的無狀态條件。主要用于無httpsession的環境中。如soap調用 </code>
<code> </code><code>if</code> <code>(stateless) { </code>
<code> </code><code>// Try to obtain from cache </code>
<code> </code><code>//通過緩存來存儲認證明體。主要避免每次請求最新ticket的網絡開銷 </code>
<code> </code><code>result = statelessTicketCache.getByTicketId(authentication.getCredentials().toString()); </code>
<code> </code><code>if</code> <code>(result == </code><code>null</code><code>) { </code>
<code> </code><code>result = </code><code>this</code><code>.authenticateNow(authentication); </code>
<code> </code><code>result.setDetails(authentication.getDetails()); </code>
<code> </code><code>// Add to cache </code>
<code> </code><code>statelessTicketCache.putTicketInCache(result); </code>
<code> </code><code>return</code> <code>result; </code>
<code>} </code>
<code>//完成認證工作 </code>
<code>private</code> <code>CasAuthenticationToken authenticateNow(</code><code>final</code> <code>Authentication authentication) </code><code>throws</code> <code>AuthenticationException { </code>
<code> </code><code>try</code> <code>{ </code>
<code> </code><code>//通過cas client的ticketValidator完成ticket校驗,并傳回身份斷言 </code>
<code> </code><code>final</code> <code>Assertion assertion = </code><code>this</code><code>.ticketValidator.validate(authentication.getCredentials().toString(), serviceProperties.getService()); </code>
<code> </code><code>//根據斷言資訊構造UserDetails </code>
<code> </code><code>final</code> <code>UserDetails userDetails = loadUserByAssertion(assertion); </code>
<code> </code><code>//檢查賬号狀态 </code>
<code> </code><code>userDetailsChecker.check(userDetails); </code>
<code> </code><code>//構造CasAuthenticationToken </code>
<code> </code><code>return</code> <code>new</code> <code>CasAuthenticationToken(</code><code>this</code><code>.key, userDetails, authentication.getCredentials(), userDetails.getAuthorities(), userDetails, assertion); </code>
<code> </code><code>} </code><code>catch</code> <code>(</code><code>final</code> <code>TicketValidationException e) { </code>
<code> </code><code>throw</code> <code>new</code> <code>BadCredentialsException(e.getMessage(), e); </code>
<code>//通過注入的authenticationUserDetailsService根據token中的認證主體即使用者名擷取UserDetails </code>
<code>protected</code> <code>UserDetails loadUserByAssertion(</code><code>final</code> <code>Assertion assertion) { </code>
<code> </code><code>final</code> <code>CasAssertionAuthenticationToken token = </code><code>new</code> <code>CasAssertionAuthenticationToken(assertion, </code><code>""</code><code>); </code>
<code> </code><code>return</code> <code>this</code><code>.authenticationUserDetailsService.loadUserDetails(token); </code>
需要注意的是為什麼要定義authenticationUserDetailsService這個bean。由于CAS需要authentication-manager标簽下定義<security:authentication-provider ref="casAuthenticationProvider"/>,而不是之前所介紹的
user-service-ref屬性,是以這裡僅僅定義了一個provider,而沒有注入UserDetailsService,是以這裡需要單獨定義authenticationUserDetailsService這個bean,并注入到CasAuthenticationProvider中。
這裡需要對CasAuthenticationToken、CasAssertionAuthenticationToken單獨解釋一下
CasAuthenticationToken:一個成功通過的CAS認證,與UsernamePasswordAuthenticationToken一樣,都是繼承于AbstractAuthenticationToken,并且最終會儲存到SecurityContext上下文、session中
CasAssertionAuthenticationToken:一個臨時的認證對象用于輔助擷取UserDetails
配置檔案中幾個bean定義這裡就不一一分析了,都是為了輔助完成CAS認證、跳轉的工作。
現在,可以對整個CAS認證的過程總結一下了:
1.用戶端發起一個請求,試圖通路系統系統中受保護的url
2.各filter鍊進行攔截并做相應處理,由于沒有通過認證,ExceptionTranslationFilter過濾器會捕獲到通路拒絕異常,并把該異常交給入口點處理
3.CAS 認證對應的入口點直接跳轉到CAS Server端的登入界面,并攜帶參數service(一般為url:……/j_spring_cas_security_check)
4.CAS Server對登入資訊進行處理,如果登入成功,就跳轉到應用系統中service指定的url,并攜帶ticket
5.應用系統中的各filter鍊再次對該url攔截,此時CasAuthenticationFilter攔截到j_spring_cas_security_check,就會對ticket進行驗證,驗證成功傳回一個身份斷言,再通過身份斷言從目前應用系統中擷取對應的UserDetails、GrantedAuthority。此時,如果步驟1中受保護的url權限清單有一個權限存在于GrantedAuthority清單中,說明有權限通路,直接響應用戶端所試圖通路的url
本文轉自布拉君君 51CTO部落格,原文連結:http://blog.51cto.com/5148737/1827795,如需轉載請自行聯系原作者