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,如需转载请自行联系原作者