基于Redis的CAS集群
單點登錄(SSO)是復雜應用系統的基本需求,Yale CAS是目前常用的開源解決方案。CAS認證中心,基于其特殊作用,自然會成為整個應用系統的核心,所有應用系統的認證工作,都將請求到CAS來完成。因此CAS服務器是整個應用的關鍵節點,CAS發生故障,所有系統都將陷入癱瘓。同時,CAS的負載能力要足夠強,能夠承擔所有的認證請求響應。利用負載均衡和集群技術,不僅能克服CAS單點故障,同時將認證請求分布到多臺CAS服務器上,有效減輕單臺CAS服務器的請求壓力。下面將基于CAS 3.4.5來討論下CAS集群。
CAS的工作原理,主要是基于票據(Ticket)來實現的(參見 CAS基本原理)。CAS票據,存儲在TicketRegistry中,因此要想實現CAS Cluster, 必須要多臺CAS之間共享所有的Ticket,采用統一的TicketRegistry,可以達到此目的。 缺省的CAS實現中,TicketRegistry在內存中實現,不同的CAS服務器有自己單獨的TicketRegistry,因此是不支持分布式集群的。但CAS提供了支持TicketRegistry分布式的接口 org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry,我們可以實現這個接口實現多臺CAS服務器TicketRegistry共享,從而實現CAS集群。
同時,較新版本CAS使用SpringWebFlow作為認證流程,而webflow需要使用session存儲流程相關信息,因此實現CAS集群,我們還得需要讓不同服務器的session進行共享。
我們采用內存數據庫Redis來實現TicketRegistry,讓多個CAS服務器共用同一個TicketRegistry。同樣方法,我們讓session也存儲在Redis中,達到共享session的目的。下面就說說如何用 Redis來實現TicketRegistry,我們使用Java調用接口Jedis來操作Redis,代碼如下: import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collection;
import org.jasig.cas.ticket.Ticket;
import org.jasig.cas.ticket.TicketGrantingTicket;
import org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/*
* TicketRegistry using Redis, to solve CAS Cluster.
*
* @author ZL
*
*/
public class RedisTicketRegistry extends AbstractDistributedTicketRegistry {
private static int redisDatabaseNum;
private static String hosts;
private static int port;
private static int st_time; //ST最大空閑時間
private static int tgt_time; //TGT最大空閑時間
private static JedisPool cachePool;
static {
redisDatabaseNum = PropertiesConfigUtil.getPropertyInt("redis_database_num");
hosts = PropertiesConfigUtil.getProperty("hosts");
port = PropertiesConfigUtil.getPropertyInt("port");
st_time = PropertiesConfigUtil.getPropertyInt("st_time");
tgt_time = PropertiesConfigUtil.getPropertyInt("tgt_time");
cachePool = new JedisPool(new JedisPoolConfig(), hosts, port);
}
public void addTicket(Ticket ticket) {
Jedis jedis = cachePool.getResource();
jedis.select(redisDatabaseNum);
int seconds = 0;
String key = ticket.getId() ;
if(ticket instanceof TicketGrantingTicket){
//key = ((TicketGrantingTicket)ticket).getAuthentication().getPrincipal().getId();
seconds = tgt_time/1000;
}else{
seconds = st_time/1000;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
try{
oos = new ObjectOutputStream(bos);
oos.writeObject(ticket);
}catch(Exception e){
log.error("adding ticket to redis error.");
}finally{
try{
if(null!=oos) oos.close();
}catch(Exception e){
log.error("oos closing error when adding ticket to redis.");
}
}
jedis.set(key.getBytes(), bos.toByteArray());
jedis.expire(key.getBytes(), seconds);
cachePool.returnResource(jedis);
}
public Ticket getTicket(final String ticketId) {
return getProxiedTicketInstance(getRawTicket(ticketId));
}
private Ticket getRawTicket(final String ticketId) {
if(null == ticketId) return null;
Jedis jedis = cachePool.getResource();
jedis.select(redisDatabaseNum);
Ticket ticket = null;
ByteArrayInputStream bais = new ByteArrayInputStream(jedis.get(ticketId.getBytes()));
ObjectInputStream ois = null;
try{
ois = new ObjectInputStream(bais);
ticket = (Ticket)ois.readObject();
}catch(Exception e){
log.error("getting ticket to redis error.");
}finally{
try{
if(null!=ois) ois.close();
}catch(Exception e){
log.error("ois closing error when getting ticket to redis.");
}
}
cachePool.returnResource(jedis);
return ticket;
}
public boolean deleteTicket(final String ticketId) {
if (ticketId == null) {
return false;
}
Jedis jedis = cachePool.getResource();
jedis.select(redisDatabaseNum);
jedis.del(ticketId.getBytes());
cachePool.returnResource(jedis);
return true;
}
public Collection<Ticket> getTickets() {
throw new UnsupportedOperationException("GetTickets not supported.");
}
protected boolean needsCallback() {
return false;
}
protected void updateTicket(final Ticket ticket) {
addTicket(ticket);
}
}
同時,我們在ticketRegistry.xml配置文件中,將TicketRegistry實現類指定為上述實現。即修改下面的class值 <!-- Ticket Registry -->
<bean id="ticketRegistry" class="org.jasig.cas.util.RedisTicketRegistry" />
<!-- <bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.DefaultTicketRegistry" />
-->
因為使用了Redis的expire功能,注釋掉如下代碼:
<!-- TICKET REGISTRY CLEANER --> lt;!-- <bean id="ticketRegistryCleaner" class="org.jasig.cas.ticket.registry.support.DefaultTicketRegistryCleaner" p:ticketRegistry-ref="ticketRegistry" /> <bean id="jobDetailTicketRegistryCleaner" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean" p:targetObject-ref="ticketRegistryCleaner" p:targetMethod="clean" /> <bean id="triggerJobDetailTicketRegistryCleaner" class="org.springframework.scheduling.quartz.SimpleTriggerBean" p:jobDetail-ref="jobDetailTicketRegistryCleaner" p:startDelay="20000" p:repeatInterval="5000000" /> -->通過上述實現TicketRegistry,多臺CAS服務器就可以共用同一個 TicketRegistry。對于如何共享session,我們可以采用現成的第三方工具tomcat-redis-session-manager直接集成即可。對于前端web服務器(如nginx),做好負載均衡配置,將認證請求分布轉發給后面多臺CAS,實現負載均衡和容錯目的。