伴随着系统上线,各种问题接踵而来,首当其冲的就是登陆问题。我先大概介绍下问题情况
经过压测我们服务器可承受的的单台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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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:[email protected]">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的横向扩展的问题也解决,并且,使用了缓存后,避免了服务器生重复生成凭证导致的系统性能问题。