cas服务端集群,相信网上资料也蛮多的,无非就是session共享,ticket共享
关于session共享 上一篇tomcat redis session manager已经给出了具体的解决方案,这里主要说一下ticket共享问题
首先定义自己的ticketRegistory 如下
public class RedisTicketRegistry extends AbstractDistributedTicketRegistry {
private static final Logger logger = LoggerFactory.getLogger(RedisTicketRegistry.class);
//st ticket 最大空闲时间
private int stMaxFreeTime;
//tgt ticket 最大空闲时间
private int tgtMaxFreeTime;
private RedisManager redisManager;
/**
* 添加票据
* Description
* @param ticket
* @see org.jasig.cas.ticket.registry.TicketRegistry#addTicket(org.jasig.cas.ticket.Ticket)
*/
@Override
public void addTicket(Ticket ticket) {
// TODO Auto-generated method stub
String key = ticket.getId();
int seconds = 0;
if(ticket instanceof TicketGrantingTicket){
seconds = this.getTgtMaxFreeTime();
} else {
seconds = this.getStMaxFreeTime();
}
logger.debug("---------------add Tikcet into redis------------------- start");
this.getRedisManager().set(SerializeUtils.serialize(key), SerializeUtils.serialize(ticket),seconds);
logger.debug("---------------add Ticket into redis------------------- end");
}
/**
* 删除票据
* Description
* @param ticketId
* @return
* @see org.jasig.cas.ticket.registry.TicketRegistry#deleteTicket(java.lang.String)
*/
@Override
public boolean deleteTicket(String ticketId) {
// TODO Auto-generated method stub
if(ticketId==null){
return false;
}
logger.debug("----------------delete Ticket from redis-------------- start");
this.getRedisManager().del(SerializeUtils.serialize(ticketId));
logger.debug("----------------delete Ticket from redis-------------- end");
return true;
}
/**
* 获取票据
* Description
* @param ticketId
* @return
* @see org.jasig.cas.ticket.registry.TicketRegistry#getTicket(java.lang.String)
*/
@Override
public Ticket getTicket(String ticketId) {
// TODO Auto-generated method stub
logger.debug("---------------get Ticket byte[] ---------------- start");
byte []tempArr = this.getRedisManager().get(SerializeUtils.serialize(ticketId));
Ticket ticket = null;
if(tempArr!=null){
logger.debug("--------------------deserialize to Ticket--------------- start");
ticket = (Ticket) SerializeUtils.deserialize(tempArr);
}
logger.debug("-----------------get Tikcet -------------------- end");
return ticket;
}
@Override
public Collection
getTickets() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("----------GetTickets not supported----------------");
}
@Override
protected boolean needsCallback() {
// TODO Auto-generated method stub
return false;
}
/**
* 修改票据
* Description
* @param ticket
* @see org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry#updateTicket(org.jasig.cas.ticket.Ticket)
*/
@Override
protected void updateTicket(Ticket ticket) {
// TODO Auto-generated method stub
this.addTicket(ticket);
}
public int getStMaxFreeTime() {
return stMaxFreeTime;
}
public void setStMaxFreeTime(int stMaxFreeTime) {
this.stMaxFreeTime = stMaxFreeTime;
}
public int getTgtMaxFreeTime() {
return tgtMaxFreeTime;
}
public void setTgtMaxFreeTime(int tgtMaxFreeTime) {
this.tgtMaxFreeTime = tgtMaxFreeTime;
}
public RedisManager getRedisManager() {
return redisManager;
}
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
}
}
然后修改ticketRegistory.xml 文件 屏蔽掉
<!-- <bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.DefaultTicketRegistry" /> -->
<!-- Quartz
TICKET REGISTRY CLEANER -->
<!-- <bean id="ticketRegistryCleaner" class="org.jasig.cas.ticket.registry.support.DefaultTicketRegistryCleaner"
p:ticketRegistry-ref="ticketRegistry"
p:logoutManager-ref="logoutManager" />
<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" /> -->
<bean id="ticketRegistry" class="com.cn.instai.cas.registry.RedisTicketRegistry"
p:redisManager-ref="redisManager"
p:stMaxFreeTime="${st.timeToKillInSeconds}"
p:tgtMaxFreeTime="${tgt.timeToKillInSeconds}"
/>
${st.timeToKillInSeconds} 与 <span style="font-family: Arial, Helvetica, sans-serif;">${tgt.timeToKillInSeconds} 根据具体情况赋值</span>
到此 ticket的共享完成
但是测试发现 单点登出虽然ticket是会清楚 但没法单点登出成功 调试发现 以这种方式共享ticket时候 logout时候没有向请求系统发送logoutRequest请求
4.0采用的是logout-webflow 流程结构,查看logout-webflow.xml
<action-state id="terminateSession">
<evaluate expression="terminateSessionAction.terminate(flowRequestContext)" />
<transition to="doLogout" />
</action-state>
找到开始节点
注意 主要登出业务处理地方 刚开始被我忽略了,一直跟着logoutAction源码 最后发现原来处理都是在terminateSession
cas-server.xml中找到 id为terminateSessionAction的bean 类为org.jasig.cas.web.flow.TerminateSessionAction
<evaluate expression="terminateSessionAction.terminate(flowRequestContext)" />这一句 其实就是调用 TerminateSessionAction 的terminate方法
代码如下
public Event terminate(final RequestContext context) {
// in login's webflow : we can get the value from context as it has already been stored
String tgtId = WebUtils.getTicketGrantingTicketId(context);
// for logout, we need to get the cookie's value
if (tgtId == null) {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
tgtId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
}
if (tgtId != null) {
WebUtils.putLogoutRequests(context, this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId));
}
final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
this.ticketGrantingTicketCookieGenerator.removeCookie(response);
this.warnCookieGenerator.removeCookie(response);
return this.eventFactorySupport.success(this);
}
发现 centralAuthenticationService.destroyTicketGrantingTicket 发现原来是调用了 centralAuthenticationService的销毁 tgt票据的方法
查看该方法
public List
destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
Assert.notNull(ticketGrantingTicketId);
logger.debug("Removing ticket [{}] from registry.", ticketGrantingTicketId);
final TicketGrantingTicket ticket = this.ticketRegistry.getTicket(ticketGrantingTicketId,
TicketGrantingTicket.class);
if (ticket == null) {
logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
return Collections.emptyList();
}
logger.debug("Ticket found. Processing logout requests and then deleting the ticket...");
//这里就是调用logoutManager的执行logout方法
final List
logoutRequests = logoutManager.performLogout(ticket);
this.ticketRegistry.deleteTicket(ticketGrantingTicketId);
return logoutRequests;
}
查看 logoutManager的performLogout方法
public List
performLogout(final TicketGrantingTicket ticket) {
final Map
services;
// synchronize the retrieval of the services and their cleaning for the TGT
// to avoid concurrent logout mess ups
synchronized (ticket) {
//获取登录过的系统servie
services = ticket.getServices();
ticket.removeAllServices();
}
ticket.markTicketExpired();
final List
logoutRequests = new ArrayList
();
// if SLO is not disabled
if (!disableSingleSignOut) {
// through all services
for (final String ticketId : services.keySet()) {
final Service service = services.get(ticketId);
// it's a SingleLogoutService, else ignore
if (service instanceof SingleLogoutService) {
final SingleLogoutService singleLogoutService = (SingleLogoutService) service;
// the logout has not performed already
if (!singleLogoutService.isLoggedOutAlready()) {
final LogoutRequest logoutRequest = new LogoutRequest(ticketId, singleLogoutService);
// always add the logout request
logoutRequests.add(logoutRequest);
final RegisteredService registeredService = servicesManager.findServiceBy(service);
// the service is no more defined, or the logout type is not defined or is back channel
if (registeredService == null || registeredService.getLogoutType() == null
|| registeredService.getLogoutType() == LogoutType.BACK_CHANNEL) {
// perform back channel logout
if (performBackChannelLogout(logoutRequest)) {
logoutRequest.setStatus(LogoutRequestStatus.SUCCESS);
} else {
logoutRequest.setStatus(LogoutRequestStatus.FAILURE);
LOGGER.warn("Logout message not sent to [{}]; Continuing processing...",
singleLogoutService.getId());
}
}
}
}
}
}
return logoutRequests;
}
发现 services = ticket.getServices(); 如果采用默认的ticketRegistry 可以获取到所有service 而如果采用RedisTicketRegistry获取到的services为空的
这里 总算是找到导致没法单点登出的原因了 两种redisTicketRegistry里面保存的TGT票据 不包含 客户端登录过的servie信息
于是查看 ServiceValidateController 开始我以为是在认证st票据的时候 存储了对应的servie信息 结果好像没发现
后来看到 C
entralAuthenticationServiceImpl中的grantServiceTicket方法
//TGT票据对授权ST票据
final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket(generatedServiceTicketId, service,
this.serviceTicketExpirationPolicy, credentials != null);
this.serviceTicketRegistry.addTicket(serviceTicket);
this.serviceTicketExpirationPolicy, credentials != null);
this.serviceTicketRegistry.addTicket(serviceTicket);
TGT票据grantServiceTicket方法
public synchronized ServiceTicket grantServiceTicket(final String id,
final Service service, final ExpirationPolicy expirationPolicy,
final boolean credentialsProvided) {
final ServiceTicket serviceTicket = new ServiceTicketImpl(id, this,
service, this.getCountOfUses() == 0 || credentialsProvided,
expirationPolicy);
updateState();
final List
authentications = getChainedAuthentications();
service.setPrincipal(authentications.get(authentications.size()-1).getPrincipal());
//把service信息 存储到services中
this.services.put(id, service);
return serviceTicket;
}
断点比较两种票据存储方式的区别 发现当使用redisTicketRegistry是 这里对TGT票据services的修改 不会更新到redis中
于是自定义了一个
centralAuthenticationService的实现类 在TGT票据授权ST票据后 对TGT票据做了一下刷新
即在ticketGrantingTicket.grantServiceTicket后面增加了一行代码
this.serviceTicketRegistry.addTicket(ticketGrantingTicket);
这里由于redis key相同会覆盖 实际效果和更新一样
到此cas服务端的集群完成
希望大神们能给出更好的解决思路