原文及更多文章请见个人博客:http://heartlifes.com
集群化方案:
1.tomcat集群共享session
2.持久化票根st及tgt
3.持久化service
4.修改ServiceManager,从内存共享改为redis共享
tomcat集群共享session
之所以要共享session,是因为cas使用了spring-webflow,而webflow使用session存储中间变量,如果不共享session,会直接导致登录流程因为缺少中间变量而失败,具体表现为输入正确用户名密码后,界面刷新重新进入登录界面。
session共享在tomcat中有三种方案可供选择,分别是:1.tomcat原始集群共享。2.redis session持久化共享。3.memcache session持久化共享。
这里我选用了tomcat原始的集群共享,原因是redis session持久化实验失败,session是成功持久化到redis中了,但是登录流程还是失败,memcache由于没有环境,没有试验。
配制tomcat集群
1.在server.xml的Engine元素下加入以下配制
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"/>
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<Membership
className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4"
port="45564"
frequency="500"
dropTime="3000"
mcastTTL="1"/>
<Receiver
className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="xxx"
port="4001"
autoBind="0"
selectorTimeout="100"
maxThreads="6"/>
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
</Channel>
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter=".*\.gif;.*\.js;.*\.jpg;.*\.htm;.*\.html;.*\.txt;"/>
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>
上面的address=”xxx”,替换成你自己的服务器ip地址
2.在工程web.xml的开头加入配置项
<distributable />
持久化票根
票根的持久化,cas默认就提供了支持,我们所要做的就是把相应的持久化类使用起来,在配置文件中替换掉原来的内存存储类。
1.pom中增加以下依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.18</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.6.2</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.5.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<scope>runtime</scope>
<version>4.2.0.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.1</version>
</dependency>
以上依赖包括mysql驱动,hibernate相关包,druid数据连接池及redis驱动(redis用于后面service持久化)
2.applicationContext.xml文件修改
增加以下配置项:
<tx:annotation-driven transaction-manager="transactionManager" />
<context:component-scan base-package="com" />
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
<property name="driverClassName" value="${jdbc.driver}" />
<!-- 基本属性 url、user、password -->
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.pool.minIdle}" />
<property name="minIdle" value="${jdbc.pool.minIdle}" />
<property name="maxActive" value="${jdbc.pool.maxActive}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="30000" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="90000" />
<property name="validationQuery" value="SELECT 'x'" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" p:dataSource-ref="dataSource" p:jpaVendorAdapter-ref="jpaVendorAdapter">
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">${database.dialect}</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
<prop key="hibernate.jdbc.batch_size">${database.batchSize}</prop>
</props>
</property>
</bean>
<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" p:generateDdl="true" p:showSql="true" />
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory" />
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
2.ticketRegistry.xml文件修改
查找ticketRegistry,修改原bean声明如下:
<bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.JpaTicketRegistry" />
持久化service
1.deployerConfigContext.xml文件修改
查找serviceRegistryDao,修改bean声明
<bean id="serviceRegistryDao" class="org.jasig.cas.services.JpaServiceRegistryDaoImpl" p:entityManagerFactory-ref="entityManagerFactory" />
修改ServiceManger
cas自带的DefaultServicesManagerImpl的集群模式,是通过直接将所有的service存在内存的一个set中,然后通过quartz,每两分钟reload一把全量service,这种2分钟同步一次service的集群模式,显然不能正式上线使用,这里我们通过自己实现ServiceManager,采用redis,对所有service进行统一管理。
1.增加RedisServicesManagerImpl类:
public class RedisServicesManagerImpl implements ServicesManager {
private final Logger log = LoggerFactory.getLogger(getClass());
private final static String SPLIT = ",";
private final static String REDIS_KEY = "registedService";
@NotNull
private ServiceRegistryDao serviceRegistryDao;
private RegisteredService disabledRegisteredService;
private RedisTemplate<String, RegisteredService> redisTemplate;
private RegexRegisteredService defaultService = new RegexRegisteredService();
public RedisServicesManagerImpl(final ServiceRegistryDao serviceRegistryDao, final RedisTemplate<String, RegisteredService> redisTemplate) {
this.serviceRegistryDao = serviceRegistryDao;
this.disabledRegisteredService = constructDefaultRegisteredService(new ArrayList<String>());
this.redisTemplate = redisTemplate;
constructDefaultService();
}
@Transactional(readOnly = false)
@Audit(action = "DELETE_SERVICE", actionResolverName = "DELETE_SERVICE_ACTION_RESOLVER", resourceResolverName = "DELETE_SERVICE_RESOURCE_RESOLVER")
public synchronized RegisteredService delete(final long id) {
final RegisteredService r = findServiceBy(id);
if (r == null) {
return null;
}
log.info("delete service by id..." + r.getServiceId() + "..."
+ r.getId());
this.serviceRegistryDao.delete(r);
String key = r.getId() + SPLIT + r.getServiceId();
// redisTemplate.opsForValue().getOperations().delete(key);
redisTemplate.opsForHash().delete(REDIS_KEY, key);
return r;
}
public RegisteredService findServiceBy(final Service service) {
if (service != null) {
log.info("find service by service..." + service.getId() + "..."
+ service.getClass());
}
Collection<RegisteredService> c = getAllServices();
if (c.isEmpty()) {
log.info("find service by service...service is blank");
return this.disabledRegisteredService;
}
for (final RegisteredService r : c) {
if (r.matches(service)) {
log.info("find service by service...service is a match...in service..."
+ service.getId()
+ "...with redis..."
+ r.getServiceId());
return r;
}
}
log.info("find service by service...service not match");
return null;
}
public RegisteredService findServiceBy(final long id) {
log.info("find service by id..." + id);
Map<Object, Object> map = redisTemplate.opsForHash().entries(REDIS_KEY);
Set<Entry<Object, Object>> set = map.entrySet();
Iterator<Entry<Object, Object>> it = set.iterator();
while (it.hasNext()) {
Entry<Object, Object> entry = it.next();
String key = entry.getKey().toString();
RegisteredService value = (RegisteredService) entry.getKey();
log.info("find service by id...service in redis..." + key);
if (String.valueOf(id).equals(key.split(SPLIT)[0])) {
log.info("find service by id...match..." + key);
try {
return (RegisteredService) value.clone();
} catch (final CloneNotSupportedException e) {
return value;
}
}
}
return null;
}
public Collection<RegisteredService> getAllServices() {
log.info("get all services...");
Set<RegisteredService> services = new TreeSet<RegisteredService>();
Map<Object, Object> map = redisTemplate.opsForHash().entries(REDIS_KEY);
Set<Entry<Object, Object>> set = map.entrySet();
Iterator<Entry<Object, Object>> it = set.iterator();
while (it.hasNext()) {
Entry<Object, Object> entry = it.next();
log.info("get all services...service in redis..." + entry.getKey()
+ "..." + entry.getValue().getClass());
String key = entry.getKey().toString();
if (entry.getValue() instanceof RegisteredService) {
RegisteredService value = (RegisteredService) entry.getValue();
log.info("get all services...service in redis..." + key);
services.add(value);
}
}
if (!services.contains(defaultService)) {
services.add(defaultService);
}
return services;
}
public boolean matchesExistingService(final Service service) {
return findServiceBy(service) != null;
}
@Transactional(readOnly = false)
@Audit(action = "SAVE_SERVICE", actionResolverName = "SAVE_SERVICE_ACTION_RESOLVER", resourceResolverName = "SAVE_SERVICE_RESOURCE_RESOLVER")
public synchronized RegisteredService save(
final RegisteredService registeredService) {
log.info("save service..." + registeredService.getServiceId());
final RegisteredService r = this.serviceRegistryDao
.save(registeredService);
String key = registeredService.getId() + SPLIT
+ registeredService.getServiceId();
log.info("save service in redis..." + key);
// redisTemplate.opsForValue().set(key, registeredService);
redisTemplate.opsForHash().put(REDIS_KEY, key, registeredService);
return r;
}
private RegisteredService constructDefaultRegisteredService(
final List<String> attributes) {
final RegisteredServiceImpl r = new RegisteredServiceImpl();
r.setAllowedToProxy(true);
r.setAnonymousAccess(false);
r.setEnabled(true);
r.setSsoEnabled(true);
r.setAllowedAttributes(attributes);
if (attributes == null || attributes.isEmpty()) {
r.setIgnoreAttributes(true);
}
return r;
}
private void constructDefaultService() {
defaultService.setId(0);
defaultService.setName("HTTP and IMAP");
defaultService.setDescription("Allows HTTP(S) and IMAP(S) protocols");
defaultService.setServiceId("^(https?|imaps?)://.*");
defaultService.setEvaluationOrder(10000001);
}
}
2.applicationContext.xml文件修改
增加以下配置项:
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="${redis.pool.maxActive}" />
<property name="maxIdle" value="${redis.pool.maxIdle}" />
<property name="maxWaitMillis" value="${redis.pool.maxWait}" />
<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.hostname}" />
<property name="port" value="${redis.port}" />
<property name="password" value="${redis.password}" />
<property name="poolConfig" ref="jedisPoolConfig" />
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
修改以下配置项:
<bean id="servicesManager" class="com.wondersgroup.cas.services.RedisServicesManagerImpl">
<constructor-arg index="0" ref="serviceRegistryDao" />
<constructor-arg index="1" ref="redisTemplate" />
</bean>