伴随着系统上线,各种问题接踵而来,首当其冲的就是登陆问题。我先大概介绍下问题情况
经过压测我们服务器可承受的的单台CAS的最大并发数是700用户,但是按照原先设计最起码要求达到1400-2000左右,我们配置了2台CAS服务器,架构上WEB负载采用的是硬件A10进行轮播,1400是可以达到的,但是随着并发的提高经常会出现401的问题。经过漫长的源码分析最后发现了问题,CAS的默认配置中,他无法横向扩展。也就是说,通过CAS来产生登录的凭证只能本机生效,现在有2台CAS服务器,用户登是用的A服务器的产生的凭证,这个时候如果访问某个应用的时候需要验证单点A10轮播到了B服务器,那么B服务器上是没有凭证的,这个时候就会出现凭证不存在的问题(浏览器401)。
问题既然找到了,那么就好办了,最后综合考虑,我们添加了一台redis缓存服务器用来存放凭证,并且对CAS的源代码进行了扩展达到了凭证共享。
代码上的操作如下。
1.修改WEB-INF\spring-configuration\ticketRegistry.xml文件。修改注册方式
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<description>
Configuration for the default TicketRegistry which stores the tickets in-memory and cleans them out as specified intervals.
</description>
<!-- 修改注册方式改为自己扩展原生的CAS注册代码-->
<bean id="ticketRegistry" class="com.cas.ticket.RedisTicketRegistry" />
<!--Quartz -->
<!-- TICKET REGISTRY CLEANER -->
<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" />
</beans>
2.com.cas.ticket.RedisTicketRegistry讲解
package com.cas.ticket;
import org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry;
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;
/***
*
* @Title: RedisTicketRegistry
* @Description: TODO(CAS分布式注册凭证生成)
* @Copyright: Copyright (c) 2015-2020
* @Company:CCS
* @author sunwenbo
* @date 2018年5月23日 下午11:55:48
*
*/
public class RedisTicketRegistry extends AbstractDistributedTicketRegistry {
/**
* ST最大空闲时间
*/
private static int st_time;
/**
* TGT最大空闲时间
*/
private static int tgt_time;
/***
* (非 Javadoc)
* <p>Title: addTicket</p>
* <p>Description:注册凭证 </p>
* @param ticket
* @see org.jasig.cas.ticket.registry.TicketRegistry#addTicket(org.jasig.cas.ticket.Ticket)
*/
@Override
public void addTicket(Ticket ticket) {
//设置redis失效时间,这个必须设置,不然容易造成缓存节点爆掉
int seconds = 3600 * 12 * 1000;
String key = ticket.getId();
if (ticket instanceof TicketGrantingTicket) {
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) {
e.printStackTrace();
System.out.println("adding ticket to redis error.");
} finally {
try {
if (null != oos)
oos.close();
} catch (Exception e) {
e.printStackTrace();
System.out
.println("oos closing error when adding ticket to redis.");
}
}
//设置redis,这个是自己写的工具类,也可以自己写个工具类
RedisUtils.setObj(key, ticket, seconds);
}
/***
* (非 Javadoc)
* <p>Title: deleteTicket</p>
* <p>Description: 删除凭证</p>
* @param ticketId
* @return
* @see org.jasig.cas.ticket.registry.TicketRegistry#deleteTicket(java.lang.String)
*/
@Override
public boolean deleteTicket(String ticketId) {
if (ticketId == null) {
return false;
}
RedisUtils.del(ticketId);
return true;
}
@Override
public Ticket getTicket(String ticketId) {
return getRawTicket(ticketId);
}
/***
*
* @Title: getRawTicket
* @Description: TODO(这个是关键的方法,CAS登录会调用此方法验证)
* @param @param ticketId
* @param @return 设定参数
* @return Ticket 返回类型
* @throws
*/
private Ticket getRawTicket(final String ticketId) {
if (null == ticketId) {
return null;
}
if (RedisUtils.getObj(ticketId) == null) {
return null;
}
Ticket ticket = (Ticket) RedisUtils.getObj(ticketId);
return ticket;
}
@Override
public Collection<Ticket> getTickets() {
throw new UnsupportedOperationException("GetTickets not supported.");
}
protected boolean needsCallback() {
return false;
}
@Override
protected void updateTicket(Ticket ticket) {
this.addTicket(ticket);
}
}
3.redis配置文件放在webapp\WEB-INF\spring-configuration目录下,在WEB.XML中配置下容器启动的时候回自动扫描。
4.RedisUtils工具类
package com.cas.ticket;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import redis.clients.jedis.JedisCluster;
public class RedisUtils {
public JedisCluster jedisCluster;
private static RedisUtils redisUtils;
public void setJedisCluster(JedisCluster jedisCluster) {
this.jedisCluster = jedisCluster;
}
public void init() {
redisUtils = this;
redisUtils.jedisCluster = this.jedisCluster;
}
/**
*
* TODO 基本写入缓存方法
*
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param key
* @param value
* @param cacheSeconds
* 单位:秒(永不超时置为0)
*/
public static String set(String key, String value, int cacheSeconds) {
if (cacheSeconds == 0) {
return redisUtils.jedisCluster.set(key, value);
} else {
return redisUtils.jedisCluster.setex(key, cacheSeconds, value);
}
}
/**
*
* TODO 基本的取值方法
*
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param key
* @return
*/
public static String get(String key) {
return redisUtils.jedisCluster.get(key);
}
/**
*
* TODO redis删除缓存
*
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param key
*/
public static void del(String key) {
redisUtils.jedisCluster.del(key);
redisUtils.jedisCluster.del(key.getBytes());
}
/**
*
* TODO 是否存在指定key的缓存
*
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param key
* @return
*/
public static Boolean exists(String key) {
return redisUtils.jedisCluster.exists(key)
|| redisUtils.jedisCluster.exists(key.getBytes());
}
/**
*
* TODO 缓存redis存入单个对象
*
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param key
* @param obj
* @param cacheSeconds
* 单位:秒(永不超时置为0)
*/
public static String setObj(String key, Object obj, int cacheSeconds) {
String s="";
if (cacheSeconds == 0) {
s=redisUtils.jedisCluster.set(key.getBytes(), serialize(obj));
return s;
} else {
s=redisUtils.jedisCluster.setex(key.getBytes(), cacheSeconds,
serialize(obj));
return s;
}
}
/**
*
* TODO 缓存redis获取单个对象
*
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param key
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T getObj(String key) {
return (T) deserialize(redisUtils.jedisCluster.get(key.getBytes()));
}
/**
*
* TODO 缓存redis存入对象List
*
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param <T>
* @param key
* @param obj
* @param cacheSeconds
* 单位:秒(永不超时置为0)
*/
public static <T> void setObjList(String key, List<T> objList,
int cacheSeconds) {
redisUtils.jedisCluster.del(key.getBytes());
for (Object o : objList) {
redisUtils.jedisCluster.rpush(key.getBytes(), serialize(o));
}
if (cacheSeconds != 0) {
redisUtils.jedisCluster.expire(key.getBytes(), cacheSeconds);
}
}
/**
*
* TODO 缓存redis获取多个对象
*
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param key
* @return
*/
@SuppressWarnings("unchecked")
public static <T> List<T> getObjList(String key) {
List<byte[]> lrange = redisUtils.jedisCluster.lrange(key.getBytes(), 0,
-1);
List<T> list = new ArrayList<T>();
for (byte[] b : lrange) {
list.add((T) deserialize(b));
}
return list;
}
/**
*
* TODO 批量插入
* @Title com.paas.common.jedis.RedisUtils.java
* @author <a href="mailto:497655157@qq.com">chengyanhua</a>
* @param map(其中每对键值对应一条缓存)
*/
public static void batchSetObject(Map<String, Object> map) {
JedisClusterPipeline jcp = JedisClusterPipeline
.pipelined(redisUtils.jedisCluster);
jcp.refreshCluster();
try {
for (Entry en : map.entrySet()) {
jcp.set(en.getKey().toString().getBytes(), serialize(en.getValue()));
}
jcp.syncAndReturnAll();
}finally {
jcp.close();
}
}
public static void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception e) {
}
}
}
public static byte[] serialize(Object value) {
if (value == null) {
throw new NullPointerException("Can't serialize null");
}
byte[] result = null;
ByteArrayOutputStream bos = null;
ObjectOutputStream os = null;
try {
bos = new ByteArrayOutputStream();
os = new ObjectOutputStream(bos);
os.writeObject(value);
os.close();
bos.close();
result = bos.toByteArray();
} catch (IOException e) {
throw new IllegalArgumentException("Non-serializable object", e);
} finally {
close(os);
close(bos);
}
return result;
}
public static Object deserialize(byte[] in) {
Object result = null;
ByteArrayInputStream bis = null;
ObjectInputStream is = null;
try {
if (in != null) {
bis = new ByteArrayInputStream(in);
is = new ObjectInputStream(bis);
result = is.readObject();
is.close();
bis.close();
}
} catch (IOException e) {
throw new IllegalArgumentException(
"Caught IOException decoding %d bytes of data", e);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
"Caught CNFE decoding %d bytes of data", e);
} finally {
close(is);
close(bis);
}
return result;
}
}
写完后,发布,会发现不仅仅401的问题解决了,同时CAS的横向扩展的问题也解决,并且,使用了缓存后,避免了服务器生重复生成凭证导致的系统性能问题。