天天看點

spring security 與cas 結合

Spring Security3對CAS的支援主要在這個spring-security-cas-client-3.0.2.RELEASE.jar包中 

Spring Security和CAS內建的配置資料很多。這裡講解的比較詳細 

http://lengyun3566.iteye.com/blog/1358323

配置方面,主要為下面的部分:

<code>&lt;</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>&gt;  </code>

<code>    </code><code>&lt;</code><code>security:custom-filter</code> <code>ref</code><code>=</code><code>"casAuthenticationFilter"</code> <code>position</code><code>=</code><code>"CAS_FILTER"</code><code>/&gt;  </code>

<code>    </code><code>&lt;</code><code>security:form-login</code> <code>login-page</code><code>=</code><code>"/login.jsp"</code><code>/&gt;  </code>

<code>    </code><code>&lt;</code><code>security:logout</code> <code>logout-success-url</code><code>=</code><code>"/login.jsp"</code><code>/&gt;  </code>

<code>    </code><code>&lt;</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>/&gt;  </code>

<code>    </code><code>&lt;</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>/&gt;  </code>

<code>    </code><code>&lt;</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>/&gt;  </code>

<code>    </code><code>&lt;</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>/&gt;   </code>

<code>&lt;/</code><code>security:http</code><code>&gt; </code>

<code>&lt;</code><code>security:authentication-manager</code> <code>alias</code><code>=</code><code>"authenticationmanager"</code><code>&gt;  </code>

<code>    </code><code>&lt;</code><code>security:authentication-provider</code> <code>ref</code><code>=</code><code>"casAuthenticationProvider"</code><code>/&gt;  </code>

<code>&lt;/</code><code>security:authentication-manager</code><code>&gt;  </code>

<code>  </code> 

<code>&lt;</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>&gt;  </code>

<code>        </code><code>&lt;</code><code>property</code> <code>name</code><code>=</code><code>"ticketValidator"</code> <code>ref</code><code>=</code><code>"casTicketValidator"</code><code>/&gt;  </code>

<code>        </code><code>&lt;</code><code>property</code> <code>name</code><code>=</code><code>"serviceProperties"</code> <code>ref</code><code>=</code><code>"casService"</code><code>/&gt;  </code>

<code>        </code><code>&lt;</code><code>property</code> <code>name</code><code>=</code><code>"key"</code> <code>value</code><code>=</code><code>"docms"</code><code>/&gt;  </code>

<code>        </code><code>&lt;</code><code>property</code> <code>name</code><code>=</code><code>"authenticationUserDetailsService"</code> <code>ref</code><code>=</code><code>"authenticationUserDetailsService"</code><code>/&gt;  </code>

<code>&lt;/</code><code>bean</code><code>&gt;  </code>

<code>&lt;</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>&gt;  </code>

<code>        </code><code>&lt;</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>/&gt;  </code>

<code>&lt;/</code><code>bean</code><code>&gt;     </code>

<code>&lt;</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>&gt;  </code>

<code>    </code><code>&lt;</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>/&gt;  </code>

<code>&lt;</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>&gt;  </code>

<code>        </code><code>&lt;</code><code>property</code> <code>name</code><code>=</code><code>"authenticationManager"</code> <code>ref</code><code>=</code><code>"authenticationmanager"</code><code>/&gt;  </code>

<code>&lt;</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>&gt;  </code>

<code>        </code><code>&lt;</code><code>constructor-arg</code> <code>value</code><code>=</code><code>"https://server:8443/cas/"</code><code>/&gt;  </code>

<code>&lt;</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>&gt;  </code>

<code>        </code><code>&lt;</code><code>property</code> <code>name</code><code>=</code><code>"userDetailsService"</code> <code>ref</code><code>=</code><code>"userDetailsManager"</code><code>/&gt;  </code>

<code>&lt;/</code><code>bean</code><code>&gt;</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>&lt;</code><code>security:authentication-provider</code> <code>ref</code><code>=</code><code>"casAuthenticationProvider"</code><code>/&gt;  </code>

<code>&lt;/</code><code>security:authentication-manager</code><code>&gt;</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标簽下定義&lt;security:authentication-provider ref="casAuthenticationProvider"/&gt;,而不是之前所介紹的

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,如需轉載請自行聯系原作者