近期公司决定使用CAS来做单点登录,选择较为稳定的4.*版本,然而在集群部署时发现了不少问题。今天在这里总结一下,

一、部署条件
  • 安装tomcat7 集群 (demo 两台)
  • 安装redis 集群 (demo 单台) 
  • cas 4.2.7版本,单台CAS服务环境下登录、单点、退出皆运行正常
  • 安装nginx ,配置tomcat集群负载均衡;其他负载均衡方式皆可
  • JDK1.7
    
二、集群部署问题
  • 需解决集群环境下CAS服务集群session共享问题。
  • 需解决集群环境下多个CAS服务共用一个ticket库。
  • 需解决集群环境和springwebflow框架下CAS登录流程数据加密秘钥统一(或去除登录流程数据加密)
  • 需解决集群环境和springwebflow框架下CAS登录票据加密秘钥统一(或去除票据数据加密)
三、操作部署
  1.  配置tomcat服务器session共享:
         DEMO简单配置:
        (1)nginx配置两个tomcat服务器,server1、server2
        (2)分别向两个tomcat导入所用jar包:E:\*** \***tomcat7***\lib 目录下。
                 commons-pool-1.6.jar、jedis-2.0.0.jar、tomcat-redis-session-manager-1.2-tomcat-7-java-7.jar
        (3)分别修改tomcat配置文件:E:\*** \***tomcat7***\conf 目录下context.xml。
                <Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve"/>
                <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" 
                 database="0"
                 host="127.0.0.1"
                 maxInactiveInterval="60" 
                 port="6379"/>
        (4)为了方便测试可修改服务器欢迎页:E:\*** \***tomcat7***\webapps\ROOT\index.jsp。
                可删除ROOT目录下文件、重新写一个简单的index.jsp、增加服务器标识、展示sessionId。
                启动两台tomcat服务器和nginx。访问服务器欢迎页显示两台服务器sessionId一致即可。
                
                
 2. CAS登录流程数据加密秘钥统一:
            (1) 简要说明:
                      流程数据默认采用对称加密方式(AES),无手动配置的情况下会默认生成随机encryptionKey和signKey,
                      那么集群条件下就会产生问题,当集群中某个CAS服务在生成加密数据后去另一台CAS服务去解密,由于签名秘钥
                      分别由不同服务器随机产生的解密过程中就会产生错误。
            
             (2) 涉及代码说明:
                    
                     
CAS4.2.7版本使用spring web flow框架,在执行登录流程调用loginFlowCipherBean对流程数据进行加密解密                         时 使 用构造方法中默认注入的cipherExecutor子类 webflowCipherExecutor
                    
                    webflowCipherExecutor使用时调用了构造方法,在构造方法中又调用该类的父类BinaryCipherExecutor,此时若无手动配置,
                    构造参数的secretKeyEncryption和secretKeySigning则皆为空。在BinaryCipherExecutor的构造方法中又判断这两个构造参数,若参数
                    为空则会产生随机key值用来加密解密。
            (3) 解决方法一:
                    在CAS的配置文件cas.properties中打开webflow.encryption.key、webflow.signing.key注释,并配置值,将KEY值固定。
                    
                    那么如何获取生成固定KEY值呢,其实源码BinaryCipherExecutor类中已经告诉我们了。自己写个main方法,导入jose4j的jar包就可以随机
                    生成。
                  
                   public static void main(String[] args) {
                         final OctetSequenceJsonWebKey octetKey = OctJwkGenerator.generateJwk(512);
                         final Map<String, Object> params = octetKey.toParams(org.jose4j.jwk.JsonWebKey.OutputControlLevel.INCLUDE_SYMMETRIC);
                         String signingKey = params.get("k").toString();
                         System.out.println("webflow.signing.key:"+signingKey);
        
                         String encryptionKey = RandomStringUtils.randomAlphabetic(16);
                         System.out.println("webflow.encryption.key:"+encryptionKey);
                    }
                    
                    key的长度在BinaryCipherExecutor已有声明
            
           (4) 解决方法二:
                    去除登录流程数据加密,CAS的util包中有一个类NoOpCipherExecutor.java和BinaryCipherExecutor一样是CipherExecutor子类,省略了对
                    数据的加密解密过程。
                    
                    在CAS的配置文件cas-servlet.xml中配置:
                    <bean id="loginFlowCipherBean"  class="org.jasig.cas.web.flow.CasWebflowCipherBean">
                            <constructor-arg ref="noOpByteCipherExecutor" />
                    </bean>
                    启动服务时找不到这个类,则在applicationContext.xml中配置
                    <bean id="noOpByteCipherExecutor"  class="org.jasig.cas.util.NoOpByteCipherExecutor"/>
                    其实,自己也可以仿照NoOpCipherExecutor自定义一个CipherExecutor,打成jar包,写进配置文件。
          3. CAS登录cookie票据TGC加密秘钥统一:
            
             (1) 简要说明:
                       和CAS登录流程数据加密解密的问题一样。
                
             (2) 涉及代码说明:
                       登录验证成功后会生成TGC票据存储到浏览器的cookie中。
                       生成cookie时调用sendTicketGrantingTicketAction,然后再调用CookieRetrievingCookieGenerator
                       的addCookie方法,由于ticketGrantingTicketCookieGenerator注入了CookieRetrievingCookieGenerator
                       子类TGCCookieRetrievingCookieGenerator,其构造方法中注入了默认的cookieValueManager
                      
                      ,即是DefaultCasCookieValueManager。所以在执行CookieRetrievingCookieGenerator的addCookie方法时
                      所调用的实际上是CookieValueManager的子类DefaultCasCookieValueManager的buildCookieValue方法。
                      DefaultCasCookieValueManager构造方法中注入了defaultCookieCipherExecutor,在配置文件deployerConfigContext.xml
                      中默认配置的是TGCCipherExecutor,
                    
所以在DefaultCasCookieValueManager的buildCookieValue方法中实际调用了TGCCipherExecutor的encode方法,即是
                      TGCCipherExecutor的父类BaseStringCipherExecutor的endcode方法。在调用TGCCipherExecutor时,
                    
                      若无手动配置,其
                      构造参数secretKeyEncryption,secretKeySigning皆为空,又调用了其父类,BaseStringCipherExecutor的构造方法中对
                      这两个构造参数进行了判断,若参数为空则会产生随机key值用来加密解密。
                   
           (3) 解决方法一:
                     在CAS的配置文件cas.properties中打开tgc.encryption.key、tgc.signing.key注释,并配置值,将KEY值固定。
                            
                      那么如何获取生成固定KEY值呢,其实源码BaseStringCipherExecutor类中也已经告诉我们了。自己写个main方法,导入
                      jose4j的jar包就可以随机生成。
                      public static void main(String[] args) {
                          final OctetSequenceJsonWebKey octetKey = OctJwkGenerator.generateJwk(512);
                          final Map<String, Object> params =  octetKey.toParams(org.jose4j.jwk.JsonWebKey.OutputControlLevel.INCLUDE_SYMMETRIC);
                          String signingKey = params.get("k").toString();
                          System.out.println("tgc.signing.key:"+signingKey);
        
                         final OctetSequenceJsonWebKey octetKey2 = OctJwkGenerator.generateJwk(256);
                         final Map<String, Object> params2 = octetKey2.toParams(org.jose4j.jwk.JsonWebKey.OutputControlLevel.INCLUDE_SYMMETRIC);
                         String signingKey2 = params2.get("k").toString();
                         System.out.println("tgc.encryption.key:"+signingKey2);
                     }
            (4) 解决方法二:
                     去除票据数据加密,CAS中有一个类noOpCookieValueManager,省略了对票据的加密解密过程。可在deployerConfigContext.xml文件中配置
                    <bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.TGCCookieRetrievingCookieGenerator">
                            <constructor-arg  ref="noOpCookieValueManager" />
                    </bean>
            
          4. 使用redis作为集群公共缓存ticket的仓库:
            
            (1) 简要说明:
                      单独CAS生成TGT票据时默认缓存在所在服务器内存中,所以集群环境下如果生成票据和认证票据的请求在不同的服务器上,那么会造成票据
                      认证错误。
            (2) 解决方法:
                     ① 可建立本地工程,新建 AbstractDistributedTicketRegistry 的继承类 :
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    import javax.validation.constraints.Min;
    import javax.validation.constraints.NotNull;
    import org.jasig.cas.ticket.ServiceTicket;
    import org.jasig.cas.ticket.Ticket;
    import org.jasig.cas.ticket.TicketGrantingTicket;
    import org.jasig.cas.ticket.registry.AbstractDistributedTicketRegistry;
    import org.springframework.beans.factory.DisposableBean;
    public class RedisTicketRegistry  extends AbstractDistributedTicketRegistry implements DisposableBean {
    /** redis client. */
    @NotNull
    private final TicketRedisTemplate redisTemplate;
    /**
     * TGT cache entry timeout in seconds.
     */
    @Min(0)
    private final int tgtTimeout;
    /**
     * ST cache entry timeout in seconds.
     */
    @Min(0)
    private final int stTimeout;
    private final String PREFIX_CAS = "CAS:TICKET:";
    public RedisTicketRegistry(TicketRedisTemplate redisTemplate, int tgtTimeout, int stTimeout) {
        this.redisTemplate = redisTemplate;
        this.tgtTimeout = tgtTimeout;
        this.stTimeout = stTimeout;
    }
     @Override
     public void addTicket(Ticket ticket) {
          logger.info("Add ticket {}", ticket);
        try {
             this.redisTemplate.boundValueOps(PREFIX_CAS+ticket.getId()).set(ticket, getTimeout(ticket), TimeUnit.SECONDS);
        } catch (Exception e) {
          e.printStackTrace();
        }
     }
     @Override
     public Ticket getTicket(String ticketId) {
          logger.info("Get ticket {}", ticketId);
          try {
               Ticket t = (Ticket)this.redisTemplate.boundValueOps(PREFIX_CAS+ticketId).get();
               if (t != null) {
                return getProxiedTicketInstance(t);
            }
        } catch (final Exception e) {
          logger.error("Failed fetching {} ", ticketId, e);
        }
        return null;
     }
     @Override
     public Collection<Ticket> getTickets() {
          Set<Ticket> tickets = new HashSet<Ticket>();
         Set<String> keys = this.redisTemplate.keys(PREFIX_CAS+"*");
         for (String key : keys) {
           Ticket ticket = (Ticket)this.redisTemplate.boundValueOps(key).get();
           if (ticket == null)
             this.redisTemplate.delete(key);
           else {
             tickets.add(ticket);
           }
         }
        return tickets;
     }
     @Override
     protected boolean needsCallback() {
          // TODO Auto-generated method stub
          return true;
     }
     @Override
     protected void updateTicket(Ticket ticket) {
          logger.info("Updating ticket {}",ticket);
        try {
             this.redisTemplate.boundValueOps(PREFIX_CAS+ticket.getId()).set(ticket, getTimeout(ticket), TimeUnit.SECONDS);
        } catch (final Exception e) {
           logger.error("Failed updating {}", ticket, e);
        }
     }
     @Override
     public boolean deleteSingleTicket(String ticketId) {
          logger.debug("Deleting Single Ticket {}", ticketId);
        try {
          
            this.redisTemplate.delete(PREFIX_CAS+ticketId);
            return true;
        } catch (final Exception e) {
           logger.error("Failed deleting {}", ticketId, e);
        }
        return false;
     }
     @Override
     public void destroy() throws Exception {
          // TODO Auto-generated method stub
     }
     /**
     * Gets the timeout value for the ticket.
     *
     * @param t the t
     * @return the timeout
     */
    private int getTimeout(final Ticket t) {
        if (t instanceof TicketGrantingTicket) {
            return this.tgtTimeout;
        } else if (t instanceof ServiceTicket) {
            return this.stTimeout;
        }
        throw new IllegalArgumentException("Invalid ticket type");
    }
   }
  ② 新建RedisTemplate的继承类
     import org.jasig.cas.ticket.Ticket;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    public class TicketRedisTemplate extends RedisTemplate<String, Ticket>{
        public TicketRedisTemplate()
          {
            RedisSerializer string = new StringRedisSerializer();
            JdkSerializationRedisSerializer jdk = new JdkSerializationRedisSerializer();
            setKeySerializer(string);
            setValueSerializer(jdk);
            setHashKeySerializer(string);
            setHashValueSerializer(jdk);
          }
      public TicketRedisTemplate(RedisConnectionFactory connectionFactory)
      {
        setConnectionFactory(connectionFactory);
        afterPropertiesSet();
      }
}
        ③ 修改配置文件:
            注释掉deployerConfigContext.xml中的票据仓库配置配置:
                    
             在applicationContext.xml中加入配置
            <!-- 配置自定义票据仓库,class写入自定义文件路径-->
            <bean id="redisTicketRegistry" class="XXX.XXX.XXX.XXX.RedisTicketRegistry">
            <constructor-arg index="0" ref="redisTemplate" />
            <constructor-arg index="1" value="1800" />
            <constructor-arg index="2" value="10" />
            </bean>
            <bean id="noOpByteCipherExecutor"  class="com.hpe.cipher.executor.NoOpByteCipherExecutor"/>
            <bean id="jedisConnFactory"
              class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
              p:hostName="127.0.0.1"
              p:database="0"
              p:usePool="true"
              p:pool-config-ref="poolConfig"/>
            <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">  
            <property name="maxIdle" value="200" />  
            <property name="testOnBorrow" value="true" />  
            </bean>  
            <!-- 配置自定义模板,class写入自定义文件路径-->
            <bean id="redisTemplate" class="XXX.XXX.XXX.TicketRedisTemplate"
                p:connection-factory-ref="jedisConnFactory">
            </bean>
                 
                  
           到此,配置完成,可以完成集群部署的SSO功能。
                    

解决CAS 4.2.7 版本集群部署的各种问题相关推荐

  1. ubuntu16 redis5.0以前版本集群部署示例

    简言 1. redis5.0版本以前集群的部署是使用ruby脚本完成的,ruby脚本的安装少略麻烦(主要原因是系统自动安装的版本太低,无法部署集群,必须手动安装) 2. redis5.0版本以后把集群 ...

  2. centos7 redis5.0以后版本 集群部署示例

    简言 1. redis5.0版本以前的集群部署是使用ruby脚本完成的,略为复杂,具体示例见笔者的这篇博客,https://blog.csdn.net/yzf279533105/article/det ...

  3. ubuntu16 redis5.0以后版本集群部署示例

    简言 1. redis5.0版本以前的集群部署是使用ruby脚本完成的,略为复杂,具体示例见笔者的这篇博客,https://blog.csdn.net/yzf279533105/article/det ...

  4. centos7 redis5.0以前版本 集群部署示例 - 第一篇

    简言 1. redis5.0版本以前的集群部署是使用ruby脚本完成的,ruby脚本的安装少略麻烦(主要原因是系统自动安装的版本太低,无法部署集群,必须手动安装) 2. redis5.0版本以后的集群 ...

  5. K8S最新版本集群部署超详细(k8s版本1.5.1)docker 版本19.03.1以及基本操作和服务介绍。

    更新:今天抽时间写了昨天部署的一键脚本: date:Aug 3,2019 <Kubernetes最新版本1.15.1,shell脚本一键部署,刚刚完成测试,实用.> 最近利用空闲时间,把之 ...

  6. Centos7 Greenplum6.1开源版本集群部署

    目录 1.前言 1.1参照文档 1.2部署包 1.3服务器环境 2 准备工作 2.1 Linux用户 2.2 主机名和hosts配置 2.3 防火墙 2.4 系统资源配置 2.5 暂时启用gpadmi ...

  7. Percona-XtraDB-Cluster 5.7版本(PXC)集群部署

    PXC(Percona-XtraDB-Cluster)5.7版本集群部署 Centos 7.3系统部署Mysql 集群 PXC三个节点分别执行和安装 (1)配置hosts cat /etc/hosts ...

  8. 利用memcached实现CAS单点登录集群部署

    前言:利用memcached实现CAS单点登录集群部署 负载均衡: 将接口请求的有状态性变成无状态性.是我们在实现负载均衡时必要要解决的问题.以应用接口的session状态为例,一般解决方法都是将se ...

  9. K8S集群部署kube-Prometheus监控Ceph(版本octopus)集群、并实现告警。

    K8S集群部署kube-Prometheus监控Ceph(版本octopus)集群.并实现告警. 一.背景描述 公司K8S集群后端存储采用的是cephfs,测试环境经常性出现存储故障,虽然最后都解决了 ...

  10. 集群部署中解决定时任务重复执行的问题-redis分布式锁应用

    背景描述 有小伙伴私信我,关于存在定时任务的项目在集群环境下部署如何解决重复执行的问题,PS:定时任务没有单独拆分. 概述:之前的项目都是单机器部署,所以定时任务不会重复消费,只会执行一次.而在集群环 ...

最新文章

  1. 百度CTO王海峰博鳌解读AI“融合创新”,算力算法数据发挥综合作用
  2. Hyper-V 怎样拷贝文件至虚拟硬盘并附加到虚拟机上
  3. 【计算机网络】HTTP 与 HTTPS ( HTTPS 简介 | HTTP 通信过程 )
  4. mysql server 2012_Windows server 2012 搭建mysql双主
  5. 程序员犯的非技术错误(Top 5)
  6. atoi函数_每日干货丨C语言中的字符串处理库函数介绍与实现
  7. SpringMVC接收哪些类型参数参数
  8. Qt之线程同步(生产者消费者模式 - QSemaphore)
  9. mysql语句6_MySQL的SQL语句 - 数据操作语句(6)- INSERT 语句
  10. 华北水利水电大学计算机研究生调剂,2017年华北水利水电大学考研调剂公告
  11. zabbix添加表达式
  12. java calendar 时分秒_Java中Calendar类的常用方法(对时间进行计算的类)
  13. mybatis mysql 自增_利用Java的MyBatis框架获取MySQL中插入记录时的自增主键
  14. 截图工具当前未在计算机上运行 请重启_截图并订在屏幕上 用作对比、对照、参考,非常实用...
  15. [转]Excel插件开发基础知识
  16. 网络知识:光猫光纤宽带故障排查笔记!
  17. java编程规范换行_Java源代码的换行规则
  18. Spring cloud和规则引擎urule整合代码
  19. 数学建模基础理论【二】(定积分)
  20. peoplesoft笔记

热门文章

  1. 淘客帝国4.0免费版网页模板修改及n…
  2. 信号与线性系统翻转课堂笔记1
  3. 钉钉日志范文100篇_钉钉怎么添加日志模板 几步轻松添加
  4. 小记三款SD卡速度测试
  5. 山西毕业主题研学夏令营
  6. 如何解决数据流转「不可能三角」难题?
  7. tracert原理和使用方法
  8. Pr 添加字幕与自动字幕时间轴
  9. android 歌词同步 换行,AS3歌词同步详解
  10. 汉仪尚巍手书_汉仪尚巍手书字体