公司早期(2005年)开发的一系统S,现欲使用公司2009年左右上线的用户管理系统UM来实现用户单点登录。
技术架构现状:
系统S:Spring 1.2.5、webwork 2.1.7、xwork 1.0.5、Hibernate 3.0.5
用户管理系统UM:基于 CAS Server 3.2.1
初步考虑有如下两种集成方式:
优点:无 Spring 版本兼容性问题
缺点:无使用经验;时间久远,相关技术资源可能较难寻。
优点:有使用Spring Security 3.x 集成 CAS 的经验;Spring、Hibernate等开源组件的升级,可对系统未来的开发引入新的技术特性。
缺点:需 升级 Spring,可能因升级带来 Spring、Hibernate 等开源组件的兼容性问题。
后来我们对上述两种方式均进行了尝试并取得成功,下面仅就方式1进行说明。
先前无使用 Acegi Security 的经验,但借助Google参阅了官方参考文档和网络示例,在理解了各个组件的用途后,集成过程还是比较简单的。
整个过程主要是引入所依赖的jar和维护2个配置文件。
acegi-security-1.0.7.jar
acegi-security-cas-1.0.7.jar
casclient-2.1.0.jar
为 Acegi Security新增一个过滤器(filter),配置代码如下:
<filter>
<filter-name>acegiSecurityFilter</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>acegiSecurityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
文件中配置的是 Acegi Security 及集成CAS相关的 bean。具体bean的配置代码如下:
<!--
过滤器链,这是我们在web.xml新增的 acegiSecurityFilter 所需的FilterChainProxy的实例。
注意:这里过滤器的顺序很重要!
-->
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,anonymousProcessingFilter,casProcessingFilter,exceptionTranslationFilter,filterSecurityInterceptor,logoutFilter
</value>
</property>
</bean>
<!--
为HTTP请求,保存SecurityContext至HttpSession中。
-->
<bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">
</bean>
<!--
处理匿名访问
-->
<bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
<property name="key" value="foobar" />
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS" />
</bean>
<!--
处理HTTP资源安全访问
-->
<bean id="filterSecurityInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager">
<ref bean="authenticationManager" />
</property>
<property name="accessDecisionManager">
<ref bean="accessDecisionManager" />
</property>
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/casfailed.jsp=ROLE_ANONYMOUS
/acegi_login.jsp=ROLE_ANONYMOUS,ROLE_USER
/**/*.action=ROLE_USER
/**/*.jsp=ROLE_USER
</value>
</property>
</bean>
<bean id="accessDecisionManager" class="org.acegisecurity.vote.UnanimousBased">
<property name="decisionVoters">
<list>
<ref bean="roleVoter"/>
</list>
</property>
</bean>
<bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter">
<property name="rolePrefix">
<value>ROLE_</value>
</property>
</bean>
<bean id="casProcessingFilter" class="org.acegisecurity.ui.cas.CasProcessingFilter">
<property name="authenticationManager">
<ref bean="authenticationManager" />
</property>
<property name="authenticationFailureUrl">
<value>/casfailed.jsp</value>
</property>
<property name="defaultTargetUrl">
<value>/</value>
</property>
<property name="alwaysUseDefaultTargetUrl">
<value>false</value>
</property>
<property name="filterProcessesUrl">
<value>/j_security_check</value>
</property>
</bean>
<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="${ulic.cas.server}/logout?url=${app.url}" />
<constructor-arg>
<list>
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" />
</list>
</constructor-arg>
<property name="filterProcessesUrl">
<value>/j_security_logout</value>
</property>
</bean>
<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<ref local="casProcessingFilterEntryPoint" />
</property>
</bean>
<bean id="casProcessingFilterEntryPoint" class="org.acegisecurity.ui.cas.CasProcessingFilterEntryPoint">
<property name="serviceProperties">
<ref bean="serviceProperties" />
</property>
<property name="loginUrl">
<value>${ulic.cas.server}/login</value>
</property>
</bean>
<bean id="serviceProperties" class="org.acegisecurity.ui.cas.ServiceProperties">
<property name="service">
<value>${app.url}/j_security_check</value>
</property>
<property name="sendRenew">
<value>false</value>
</property>
</bean>
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="casAuthenticationProvider" />
<ref bean="anonymousAuthenticationProvider"/>
</list>
</property>
</bean>
<bean id="casAuthenticationProvider" class="org.acegisecurity.providers.cas.CasAuthenticationProvider">
<property name="casAuthoritiesPopulator">
<ref bean="casAuthoritiesPopulator" />
</property>
<property name="casProxyDecider">
<bean class="org.acegisecurity.providers.cas.proxy.RejectProxyTickets" />
</property>
<property name="ticketValidator">
<ref bean="casProxyTicketValidator" />
</property>
<property name="key">
<value>my_password_for_this_auth_provider_only</value>
</property>
</bean>
<bean id="anonymousAuthenticationProvider" class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
<property name="key">
<value>foobar</value>
</property>
</bean>
<bean id="casProxyTicketValidator"
class="org.acegisecurity.providers.cas.ticketvalidator.CasProxyTicketValidator">
<property name="casValidate">
<value>${ulic.cas.server}/proxyValidate</value>
</property>
<property name="serviceProperties">
<ref local="serviceProperties" />
</property>
</bean>
<bean id="casAuthoritiesPopulator" class="org.acegisecurity.providers.cas.populator.DaoCasAuthoritiesPopulator">
<property name="userDetailsService">
<ref bean="jdbcDaoImpl" />
</property>
</bean>
<bean id="jdbcDaoImpl" class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
<property name="usersByUsernameQuery">
<value>SELECT t.user_name AS username, 'nopassword' AS password, 1 AS enabled FROM t_user t WHERE t.user_name = ?</value>
</property>
<property name="authoritiesByUsernameQuery">
<value>SELECT t.user_name AS username, 'ROLE_USER' AS authority FROM t_user t WHERE t.user_name = ?</value>
</property>
</bean>
casProxyTicketValidator 的属性 casValidate 必须为一个 https 协议的地址,但公司的 CAS Server 并未配置成 https 协议,使用的是 http 协议,这样直接导致CAS验证失败。
通过覆写casclient-2.1.0.jar中的类edu.yale.its.tp.cas.util.SecureURL,取消对 https 协议的检查。
/**
* Retrieve the contents from the given URL as a String, assuming the
* URL's server matches what we expect it to match.
*/
public static String retrieve(String url) throws IOException {
if (log.isTraceEnabled()){
log.trace("entering retrieve(" + url + ")");
}
BufferedReader r = null;
try {
URL u = new URL(url);
// if (!u.getProtocol().equals("https")){
// // IOException may not be the best exception we could throw here
// // since the problem is with the URL argument we were passed, not
// // IO. -awp9
// log.error("retrieve(" + url + ") on an illegal URL since protocol was not https.");
// throw new IOException("only 'https' URLs are valid for this method");
// }
URLConnection uc = u.openConnection();
uc.setRequestProperty("Connection", "close");
r = new BufferedReader(new InputStreamReader(uc.getInputStream()));
String line;
StringBuffer buf = new StringBuffer();
while ((line = r.readLine()) != null)
buf.append(line + "\n");
return buf.toString();
} finally {
try {
if (r != null)
r.close();
} catch (IOException ex) {
// ignore
}
}
}
acegisecurity-reference-1.0.7.pdf