1. 程式人生 > >SSO CAS叢集配置

SSO CAS叢集配置

                                SSO CAS叢集配置

    該篇博文配置資訊參考自https://www.imooc.com/article/3957;
    正文:    

    如果引入了SSO,那個這個認證中心就是整個應用架構中的一個及其重要的關鍵點,它必須滿足兩個基本要求:
    1.高可用,不允許發生故障。可想而知,如果認證中心發生故障,整個應用群將無法登陸,將會導致所有服務癱瘓。
    2.高併發,因為所有使用者的登入請求都需要經過它處理,其承擔的處理量常常是相當巨大的。
因此,在實際生產系統中,認證中心這個關鍵部件通常需要進行叢集,單個認證中心提供服務是非常危險的。
當我們用CAS作為SSO解決方案時,CAS Server作為認證中心就會涉及到叢集問題。對CAS Server來說,預設是單應用例項執行的,多例項叢集執行,我們需要做特殊考慮。
    考慮叢集,就要考慮應用中有哪些點和狀態相關,這些狀態相關的點和應用的執行環境密切相關。在多例項執行下,執行環境是分散式的,這些狀態相關的點需要考慮,在分散式環境下,如何保持狀態的一致性。
鑑於CAS實現方式,狀態相關點有兩個,一是CAS登入登出流程,採用webflow實現,流程狀態儲存於session中。二是票據儲存,預設是在JVM記憶體中。
    那麼CAS叢集,我們需要保證多個例項下,session中的狀態以及票據儲存狀態,是一致的。常用的解決方案是共享,也就是說,在多CAS例項下,他們的session和票據ticket是共享的,這樣就解決了一致性問題。
CAS在Tomcat下執行的話,官方提出的建議是利用tomcat叢集進行Session複製(Session Replication)。在高併發狀態下,這種session複製效率不是很高,節點數增多時更是如此,實戰中採用較少。
    我們可以採用共享session的技術。但筆者實踐中,則採用了另外一種更靈活的方案,那就是session sticky技術。
什麼是session sticky?即將某一使用者來的請求,通過前置機合理分配,始終定位在一臺tomcat上,這樣使用者的登入登出webflow流程,始終發生在同一tomcat伺服器上,保證了狀態的完整性。實際上,採用這種方式,我們繞過了Session共享的需求。
另一個問題我們繞不過去了,那就是ticket共享問題。我們知道,ticket預設是儲存於虛擬機器記憶體中的,多個CAS Server例項,意味著多個tomcat節點,多個JVM,TicketRegistry是各自獨立不共享的。
    我們是否也可使用session sticky解決呢,不可以!因為對於ticket來說,根據認證協議,訪問ticket不僅來自瀏覽器使用者請求,而且還來自CAS Client應用系統,這是一個三方合作系統。來自應用系統的請求可能會訪問到另一個CAS Server節點從而導致狀態不一致。
因此我們要直面解決ticket共享問題。ticket的儲存由TicketRegistry定義,預設是DefaultTicketRegistry,即JVM記憶體方式實現,我們可以定義外接儲存方式,讓多個例項共用這個儲存,以達到共享目的。
外接儲存實現方式有多種選擇,如儲存在資料庫中、儲存在Cache中、儲存在記憶體資料庫中等,CAS也提供了多種實現方式的外掛,如利用memcached作為ticket儲存方式的外掛cas-server-integration-memcached、利用Cache的cas-server-integration-ehcache、cas-server-integration-jboss等。
    這裡,使用另外一種方式,即利用目前更流行的記憶體資料管理系統Redis來儲存Ticket。同時,為了保證redis的高可用和高併發處理,我們使用redis主從叢集,Sentinel控制,故認證中心具有很好的靈活性和水平可擴充套件性,整個架構圖如下:


    SSO CAS 叢集搭建

    

1.仿照cas-server-integration-memcached工程建立cas-server-integration-redis工程
q2
2.pom.xml中新增redis的java客戶端jar包,去掉memcached中需要的jar,最後依賴包如下:

<dependencies>
 <dependency>
  <groupId>org.jasig.cas</groupId>
  <artifactId>
cas-server-core</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.7.2</version> </dependency> <!-- Test dependencies --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>1.9.0</version> <scope>test</scope> </dependency> </dependencies>
  1. 定義RedisTicketRegistry類,這個是核心,它實現了TicketRegistry介面,我們使用Jedis客戶端:
public final class RedisTicketRegistry extends AbstractDistributedTicketRegistry implements DisposableBean {

 /** Redis client. */
 private JedisSentinelPool jedisPool;  
 private int st_time;  //ST最大空閒時間
 private int tgt_time; //TGT最大空閒時間

 @Override
 protected void updateTicket(final Ticket ticket) {
   logger.debug("Updating ticket {}", ticket);
   Jedis jedis = jedisPool.getResource();
   String ticketId = ticket.getId() ;
   try {
     jedis.expire(ticketId.getBytes(), getTimeout(ticket));
   }catch (final Exception e) {
    logger.error("Failed updating {}", ticket, e);
   }finally{
    jedis.close();
   }
 }

 @Override
 public void addTicket(final Ticket ticket) {

  logger.debug("Adding ticket {}", ticket);
  Jedis jedis = jedisPool.getResource();
  String ticketId = ticket.getId() ;

  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  ObjectOutputStream oos = null;
  try{
   oos = new ObjectOutputStream(bos);
   oos.writeObject(ticket);
  }catch(IOException e){
   logger.error("adding ticket {} to redis error.", ticket);
  }finally{
   try{ 
    if(null!=oos) oos.close();
   }catch(IOException e){
    logger.error("oos closing error when adding ticket {} to redis.", ticket);
   }
  }

  jedis.setex(ticketId.getBytes(), getTimeout(ticket),bos.toByteArray());
  jedis.close();
 }

 @Override
 public boolean deleteTicket(final String ticketId) {

  logger.debug("Deleting ticket {}", ticketId);
  Jedis jedis = jedisPool.getResource();
  try {
   jedis.del(ticketId.getBytes());
   return true;
  } catch (final Exception e) {
   logger.error("Failed deleting {}", ticketId, e);
   return false;
  } finally{
   jedis.close();
  }
 }

 @Override
 public Ticket getTicket(final String ticketId) {
  Jedis jedis = jedisPool.getResource();
  try {
   byte[] value = jedis.get(ticketId.getBytes());
   if (null==value){
    logger.error("Failed fetching {}, ticketId is null. ", ticketId);
    return null;
   }
   ByteArrayInputStream bais = new ByteArrayInputStream(value);
   ObjectInputStream ois = null;
   ois = new ObjectInputStream(bais);
   final Ticket  t = (Ticket)ois.readObject(); 
   if (t != null) {
    return getProxiedTicketInstance(t);
   }
  } catch (final Exception e) {
    logger.error("Failed fetching {}. ", ticketId, e);
  }finally{
    jedis.close();
  }
  return null;
 }

 /**
  * {@inheritDoc}
  * This operation is not supported.
  *
  * @throws UnsupportedOperationException if you try and call this operation.
  */
 @Override
 public Collection<Ticket> getTickets() {
   throw new UnsupportedOperationException("GetTickets not supported.");
 }

 /**
  * Destroy the client and shut down.
  *
  * @throws Exception the exception
  */
 public void destroy() throws Exception {
   jedisPool.destroy();
 }

 @Override
 protected boolean needsCallback() {
   return true;
 }

  /**
   * 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.tgt_time;
    } else if (t instanceof ServiceTicket) {
     return this.st_time;
    }
    throw new IllegalArgumentException("Invalid ticket type");
  }

 public void setSt_time(int st_time) {
   this.st_time = st_time;
  }

  public void setTgt_time(int tgt_time) {
    this.tgt_time = tgt_time;
  }

  public void setJedisSentinelPool(JedisSentinelPool jedisPool) {
    this.jedisPool = jedisPool;
  }
}

4.同理,仿照cas-server-integration-memcached編寫測試用例RedisTicketRegistryTests,核心程式碼如下:

@Test
public void testWriteGetDelete() throws Exception {
  //對ticket執行增查刪操作
  final String id = "ST-1234567890ABCDEFGHIJKL-crud";
  final ServiceTicket ticket = 
           mock(ServiceTicket.class, withSettings().serializable());
  when(ticket.getId()).thenReturn(id);
  registry.addTicket(ticket);
  final ServiceTicket ticketFromRegistry = 
                (ServiceTicket) registry.getTicket(id);
  Assert.assertNotNull(ticketFromRegistry);
  Assert.assertEquals(id, ticketFromRegistry.getId());
  registry.deleteTicket(id);
  Assert.assertNull(registry.getTicket(id));
}

相應的配置檔案ticketRegistry-test.xml定義如下:

<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">  
 <property name="maxTotal"  value="4096"/>  
 <property name="maxIdle" value="200"/>  
 <property name="maxWaitMillis" value="3000"/>
 <property name="testOnBorrow" value="true" />
 <property name="testOnReturn" value="true" />
</bean>  

<bean id="jedisSentinelPool" class="redis.clients.jedis.JedisSentinelPool">
 <constructor-arg index="0" value="mymaster" />
 <constructor-arg index="1">
   <set>  
    <value>192.168.1.111:26379</value>  
   </set> 
 </constructor-arg>
 <constructor-arg index="2" ref="poolConfig"/>
</bean>

<bean id="testCase1" class="org.jasig.cas.ticket.registry.RedisTicketRegistry" >
 <property name="jedisSentinelPool" ref="jedisSentinelPool" />
 <property name="st_time" value="10" />
 <property name="tgt_time" value="1200" />
</bean>

測試用例通過,至此,支援redis票據儲存的外掛開發完畢。然後我們利用mvn install把該外掛安裝到本地倉儲。
q3
下面我們開始在cas-server-webapp工程中使用該外掛。

5.修改cas-server-webapp工程中ticketRegistry.xml檔案,替換掉DefaultTicketRegistry,同時註釋掉ticketRegistryCleaner相關所有定義(為什麼註釋掉前文有討論)。

<bean id="ticketRegistry" 
   class="org.jasig.cas.ticket.registry.RedisTicketRegistry" >
 <property name="jedisSentinelPool" ref="jedisSentinelPool" />
 <property name="st_time" value="10" />
 <property name="tgt_time" value="1200" />
</bean>

6.在POM.xml中新增cas-server-integration-redis模組:

<dependency>
 <groupId>org.jasig.cas</groupId>
 <artifactId>cas-server-integration-redis</artifactId>
 <version>${project.version}</version>
 <scope>compile</scope>
</dependency>

7.本地啟動redis,重新build工程,然後tomcat7:run執行CAS Server。直接登入認證中心,觀察redis中資料變化。
q4
我們看到TGT存到redis中了,做登出操作,會觀察到TGT已消失。從應用系統登入,會發現ST也在redis中。