YALE CAS与集群环境整合 博客分类: java
在最近工作中使用了YALE CAS,在与集群环境整合时遇到不少问题,以下记录了遇到的问题以及解决方案。
YEAL CAS分为server和client两部分,server就是SSO登录的服务器端,它负责验证用户登录信息并生成相应的Ticket,client是各个需要集成到SSO环境中的具体应用它负责从服务端获取凭证再通过凭证从服务器端换取用户信息。下图为一个标准的单点登录流程
在集群环境下可分为client集群或者server+client集群,我使用的是server+client集群,server集群就涉及到了server节点共享Ticket问题,解决方案是使用数据库存储生成的Ticket。YEAL CAS提供了JPA 的实现,能很容易的实现Ticket数据库存储,只需修改一下配置就ok。
打开ticketRegistry.xml文件加入如下内容
<!--数据源--> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/cas" /> <!--spring的jpa支持,这里使用hibernate的jpa实现--> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="persistenceUnitName" value="cas-persistence"/> <property name="persistenceXmlLocation" value="classpath:persistence.xml"/> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="generateDdl" value="true" /> <property name="showSql" value="true" /> </bean> </property> <property name="jpaProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.DB2Dialect</prop> <prop key="hibernate.hbm2ddl.auto">none</prop> </props> </property> </bean> <!--spring的jpa事物管理--> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" /> <!--注解支持--> <tx:annotation-driven transaction-manager="transactionManager"/> <!--pojo对象注解支持--> <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/> <!--Ticket存储JPA实现--> <bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.JpaTicketRegistry" />
再打开persistence.xml文件加入如下内容
<persistence-unit name="cas-persistence" transaction-type="RESOURCE_LOCAL"> <!--ejb的hibernate实现--> <provider>org.hibernate.ejb.HibernatePersistence</provider> <!--以下内容为Ticket相关的实体对象--> <class>org.jasig.cas.services.AbstractRegisteredService</class> <class>org.jasig.cas.services.RegexRegisteredService</class> <class>org.jasig.cas.services.RegisteredServiceImpl</class> <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class> <class>org.jasig.cas.ticket.ServiceTicketImpl</class> <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class> </persistence-unit>在第1次运行时可以将hibernate.hbm2ddl.auto属性设置成create这样会自动生成相关的表。
由于现在server和client在集群环境下运行,所以在client端web.xml的cas相关filter配置中server和client ip地址配置的是转发服务器ip,这就带来一个问题,在注销操作时server无法正确注销真实的client。解决方案是在client第1次重定向到server时多传递一个client的真实ip,server在注销时根据这个ip来发送注销请求。
参考默认的AuthenticationFilter代码实现一个自己的Filter,关键代码如下
String physicalServerName = "http://" + request.getLocalAddr() + ":" + String.valueOf(request.getLocalPort()) + request.getRequestURI(); final String urlToRedirectTo = casServerLoginUrl + (casServerLoginUrl.indexOf("?") != -1 ? "&" : "?") + getServiceParameterName() + "=" + URLEncoder.encode(serviceUrl, "UTF-8") + "&physicalServer=" + URLEncoder.encode(physicalServerName , "UTF-8") + (renew ? "&renew=true" : "") + (gateway ? "&gateway=true" : "");
重定向urlToRedirectTo中在原有的参数基础上增加一个参数physicalServerName内容是当前client的ip+端口+上下文。
对应的server端的处理类有2个,分别是SimpleWebApplicationServiceImpl和CasArgumentExtractor,为了支持这个新加的参数,参考这2个类的代码实现了ClusterWebApplicationServiceImpl和ClusterCasArgumentExtractor类其中关键代码如下
ClusterWebApplicationServiceImpl
private String physicalUrl; private ClusterWebApplicationServiceImpl(final String id, final String originalUrl, final String artifactId, final ResponseType responseType, final HttpClient httpClient, String physicalUrl) { super(id, originalUrl, artifactId, httpClient); this.responseType = responseType; this.physicalUrl = physicalUrl; } public static ClusterWebApplicationServiceImpl createServiceFrom(final HttpServletRequest request, final HttpClient httpClient) { final String targetService = request.getParameter(CONST_PARAM_TARGET_SERVICE); final String method = request.getParameter(CONST_PARAM_METHOD); final String serviceToUse = StringUtils.hasText(targetService) ? targetService : request.getParameter(CONST_PARAM_SERVICE); if (!StringUtils.hasText(serviceToUse)) { return null; } final String id = cleanupUrl(serviceToUse); final String artifactId = request.getParameter(CONST_PARAM_TICKET); String physicalServer = request.getParameter(CONST_PARAM_PHYSICAL_SERVER); return new ClusterWebApplicationServiceImpl(id, serviceToUse, artifactId, "POST".equals(method) ? ResponseType.POST : ResponseType.REDIRECT, httpClient, physicalServer); } public synchronized boolean logOutOfService(final String sessionIdentifier) { if (this.loggedOutAlready) { return true; } LOG.debug("Sending logout request for: " + getId()); final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"" + GENERATOR.getNewTicketId("LR") + "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime() + "\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>" + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>"; this.loggedOutAlready = true; if (this.getHttpClient() != null) { return this.getHttpClient().sendMessageToEndPoint(physicalUrl, logoutRequest, true); } return false; }
其主要内容就是增加了一个变量physicalUrl存放客户锻真实地址,在注销方法logOutOfService中使用该地址发送注销请求。
ClusterCasArgumentExtractor的代码就很简单了内容如下
public class ClusterCasArgumentExtractor extends AbstractSingleSignOutEnabledArgumentExtractor{ @Override protected WebApplicationService extractServiceInternal( HttpServletRequest request) { return ClusterWebApplicationServiceImpl.createServiceFrom(request, getHttpClientIfSingleSignOutEnabled()); } }
最后在配置文件argumentExtractorsConfiguration.xml中替换SimpleWebApplicationServiceImpl以及argumentExtractorsConfiguration.xml中替换CasArgumentExtractor的相关配置。