加精!大型互联网应用基于CAS的SSO架构

0?wx_fmt=gif


点这里找志同道合的技术小伙伴!


前面我们对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控制,故认证中心具有很好的灵活性和水平可扩展性,整个架构图如下:



640?wx_fmt=jpeg




下面我们就一步步进行配置搭建:


1.仿照cas-server-integration-memcached工程建立cas-server-integration-redis工程


640?wx_fmt=jpeg


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>


3. 定义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把该插件安装到本地仓储。


640?wx_fmt=jpeg


下面我们开始在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中数据变化。


640?wx_fmt=jpeg


我们看到TGT存到redis中了,做登出操作,会观察到TGT已消失。从应用系统登录,会发现ST也在redis中。




640?wx_fmt=jpeg 读完本篇,你应该了解如下内容:

1.认证系统是整个应用群的关键部件,大中型应用系统要保障认证系统的高可用、高并发。


2.CAS有状态操作涉及到session和ticket,集群时要考虑这两个方面的状态一致性问题。


3.session方面可以采用session replication、session共享、session sticky等技术满足集群需要,后两种技术实战中采用更多些。


4.采用分布式内存系统解决ticket共享满足CAS集群需求。


-END-640?

欢迎关注“互联网架构师”,我们分享最有价值的互联网技术干货文章,助力您成为有思想的全栈架构师,我们只聊互联网、只聊架构,不聊其他!打造最有价值的架构师圈子和社区。

本公众号覆盖中国主要首席架构师、高级架构师、CTO、技术总监、技术负责人等人 群。分享最有价值的架构思想和内容。打造中国互联网圈最有价值的架构师圈子。

  • 长按下方的二维码可以快速关注我们

  • 640?wx_fmt=jpeg

  • 如想加群讨论学习,请点击右下角的“加群学习”菜单入群

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值