继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

《SSO CAS单点系列》之 大型互联网应用基于CAS的SSO架构(精华)

手插口袋_
关注TA
已关注
手记 25
粉丝 455
获赞 3117

作者: 常明,Java架构师
[请尊重原创,盗版必究,转载请指明出处]

f

前面我们对CAS做了相当的了解,也基本能够将CAS应用于生产环境。本篇笔者将结合自身实际工作经验,谈谈在大型互联网应用中,如何架构CAS的问题。内容绝对干货,值得珍藏:)

对于大中型互联网应用,网站性能问题提到了前所未有的高度,能够应对高并发、高可用、避免单点故障是系统架构设计的基本准则。

如果引入了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控制,故认证中心具有很好的灵活性和水平可扩展性,整个架构图如下:
q1
下面我们就一步步进行配置搭建:

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中。
q5
q
qq

本文为慕课网作者原创,转载请标明【原文作者及本文链接地址】。侵权必究,谢谢合作!
打开App,阅读手记
20人推荐
发表评论
随时随地看视频慕课网APP

热门评论

干货哦。关注你是我最大的辛福

updateTicket 方法中应该使用:

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


因为票据对应的值也需要更新,否则会导致子系统收不到退出通知。

“同时注释掉ticketRegistryCleaner相关所有定义(为什么注释掉前文有讨论)”,这个“前文”是指哪篇? 能不能实现它?求教

查看全部评论