解决CAS 4.2.7 版本集群部署的各种问题

近期公司决定使用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
                
                
                 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一致即可。
                
                解决CAS 4.2.7 版本集群部署的各种问题_第1张图片
                解决CAS 4.2.7 版本集群部署的各种问题_第2张图片
                    

                  

             2.  CAS登录流程数据加密秘钥统一

            (1) 简要说明:
                      流程数据默认采用对称加密方式(AES),无手动配置的情况下会默认生成随机encryptionKey和signKey,
                      那么集群条件下就会产生问题,当集群中某个CAS服务在生成加密数据后去另一台CAS服务去解密,由于签名秘钥
                      分别由不同服务器随机产生的解密过程中就会产生错误。
            
             (2) 涉及代码说明:
                    
                     解决CAS 4.2.7 版本集群部署的各种问题_第3张图片
                    CAS4.2.7版本使用spring web flow框架,在执行登录流程调用loginFlowCipherBean对流程数据进行加密解密                         时 使  用构造方法中默认注入的cipherExecutor子类   webflowCipherExecutor

                    解决CAS 4.2.7 版本集群部署的各种问题_第4张图片
                    webflowCipherExecutor使用时调用了构造方法,在构造方法中又调用该类的父类BinaryCipherExecutor,此时若无手动配置,
                    构造参数的secretKeyEncryptionsecretKeySigning则皆为空。在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 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 4.2.7 版本集群部署的各种问题_第5张图片
                    在CAS的配置文件cas-servlet.xml中配置:
                    
                            
                    
                    启动服务时找不到这个类,则在applicationContext.xml中配置
                    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
                      解决CAS 4.2.7 版本集群部署的各种问题_第6张图片
                      ,即是DefaultCasCookieValueManager。所以在执行CookieRetrievingCookieGenerator的addCookie方法时
                      所调用的实际上是CookieValueManager的子类DefaultCasCookieValueManager的buildCookieValue方法。
                      DefaultCasCookieValueManager构造方法中注入了defaultCookieCipherExecutor,在配置文件deployerConfigContext.xml
                      中默认配置的 TGCCipherExecutor,
                    
                       所以在 DefaultCasCookieValueManager的 buildCookieValue方法中实际调用了TGCCipherExecutor的encode方法,即是
                      TGCCipherExecutor的父类BaseStringCipherExecutor的endcode方法。在调用TGCCipherExecutor时,
                    解决CAS 4.2.7 版本集群部署的各种问题_第7张图片
                      若无手动配置,其
                      构造参数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 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 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文件中配置
                    
                            
                    
            
          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 getTickets() {
          Set tickets = new HashSet();
         Set 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{
        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="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
              p:hostName="127.0.0.1"
              p:database="0"
              p:usePool="true"
              p:pool-config-ref="poolConfig"/>
              
              
              
              
            
            
                p:connection-factory-ref="jedisConnFactory">
            
                 
                  
            到此,配置完成,可以完成集群部署的SSO功能。
                    


你可能感兴趣的:(解决CAS 4.2.7 版本集群部署的各种问题)