CAS+SSO原理浅谈

SSO  是一个非常大的主题,我对这个主题有着深深的感受,自从广州  UserGroup  的论坛成立以来,无数网友都在尝试使用开源的  CAS   Kerberos  也提供另外一种方式的  SSO  ,即基于  Windows  域的  SSO  ,还有就是从  2005 年开始一直兴旺不衰的  SAML 

       如果将这些免费的 SSO 解决方案与商业的 Tivoli  Siteminder  RSA Secure SSO 产品做对比,差距是存在的。毕竟,商业产品的安全性和用户体验都是无与伦比的,我们现在提到的 SSO ,仅仅是 Web SSO ,即Web-SSO 是体现在客户端;另外一种 SSO 是桌面 SSO ,例如,只需要作为 Administrator 登录一次 windows 2000 ,我便能够在使用 MSN/QQ 的时候免去登录的环节 ( 注意,这不是用客户端软件的密码记忆功能 ) ,是一种代理用户输入密码的功能。因此,桌面 SSO 是体现在 OS 级别上。

       今天,当我们提起 SSO 的时候,我们通常是指 Web SSO ,它的主要特点是, SSO 应用之间走 Web 协议 (  HTTP/SSL) ,并且 SSO 都只有一个登录入口。

       简单的 SSO 的体系中,会有下面三种角色:

       1  User (多个)

       2  Web 应用(多个)

       3  SSO 认证中心( 1 个)

       虽然 SSO 实现模式千奇百怪,但万变不离其宗:

l         Web 应用不处理 User 的登录,否则就是多点登陆了,所有的登录都在 SSO 认证中心进行。

l         SSO 认证中心通过一些方法来告诉 Web 应用当前访问用户究竟是不是张三 / 李四。

l         SSO 认证中心和所有的 Web 应用建立一种信任关系, SSO 认证中心对用户身份正确性的判断会通过某种方法告之 Web 应用,而且判断结果必须被 Web 应用信任。

2. CAS 的基本原理

       CAS(Central Authentication Service)  Yale 大学发起的一个开源项目,据统计,大概每 10 个采用开源构建 Web SSO  Java 项目,就有 8 个使用 CAS 。对这些统计,我虽然不以为然,但有一点可以肯定的是, CAS 是我认为最简单实效,而且足够安全的 SSO 选择。

       本节主要分析 CAS 的安全性,以及为什么 CAS 被这样设计,带着少许密码学的基础知识,我希望有助于读者对 CAS 的协议有更深层次的理解。

2.1 CAS 的结构体系

从结构体系看, CAS 包含两部分:

l         CAS Server

CAS Server 负责完成对用户的认证工作, CAS Server 需要独立部署,有不止一种 CAS Server 的实现, Yale CAS Server  ESUP CAS Server 都是很不错的选择。

CAS Server 会处理用户名 / 密码等凭证 (Credentials) ,它可能会到数据库检索一条用户帐号信息,也可能在 XML 文件中检索用户密码,对这种方式, CAS 均提供一种灵活但同一的接口 / 实现分离的方式, CAS 究竟是用何种认证方式,跟 CAS 协议是分离的,也就是,这个认证的实现细节可以自己定制和扩展。

l         CAS Client

CAS Client 负责部署在客户端(注意,我是指 Web 应用),原则上, CAS Client 的部署意味着,当有对本地 Web 应用的受保护资源的访问请求,并且需要对请求方进行身份认证, Web 应用不再接受任何的用户名密码等类似的 Credentials ,而是重定向到 CAS Server 进行认证。

目前, CAS Client 支持(某些在完善中)非常多的客户端,包括 Java  .Net  ISAPI  Php  Perl  uPortal  Acegi  Ruby  VBScript 等客户端,几乎可以这样说, CAS 协议能够适合任何语言编写的客户端应用。

2.2 CAS 协议

       剖析协议就像剖析设计模式,有些时候,协议让人摸不着头脑。 CAS 的代理模式要相对复杂一些,它引入了一些新的概念,我希望能够在这里描述一下其原理,有助于读者在配置和调试 CAS SSO 有更清晰的思路。

如果没记错, CAS 协议应该是由 Drew Mazurek 负责可开发的,从 CAS v1 到现在的 CAS v3 ,整个协议的基础思想都是基于 Kerberos 的票据方式。

       CAS v1 非常原始,传送一个用户名居然是 ”yes"ndavid.turing” 的方式, CAS v2 开始使用了 XML 规范,大大增强了可扩展性, CAS v3 开始使用 AOP 技术,让 Spring 爱好者可以轻松配置 CAS Server 到现有的应用环境中。

CAS 是通过 TGT(Ticket Granting Ticket) 来获取 ST(Service Ticket) ,通过 ST 来访问服务,而 CAS 也有对应 TGT  ST 的实体,而且他们在保护 TGT 的方法上虽然有所区别,但是,最终都可以实现这样一个目的——免去多次登录的麻烦。

       下面,我们看看 CAS 的基本协议框架:

2.1.1 基础协议

cas_protocol-1.jpg 
                                                 CAS 基础模式

       上图是一个最基础的 CAS 协议, CAS Client  Filter 方式保护 Web 应用的受保护资源,过滤从客户端过来的每一个 Web 请求,同时, CAS Client 会分析 HTTP 请求中是否包请求 Service Ticket( 上图中的Ticket) ,如果没有,则说明该用户是没有经过认证的,于是, CAS Client 会重定向用户请求到 CAS Server  Step 2 )。 Step 3 是用户认证过程,如果用户提供了正确的 Credentials  CAS Server 会产生一个随机的Service Ticket ,然后,缓存该 Ticket ,并且重定向用户到 CAS Client (附带刚才产生的 Service Ticket ), Service Ticket 是不可以伪造的,最后, Step 5  Step6  CAS Client  CAS Server 之间完成了一个对用户的身份核实,用 Ticket 查到 Username ,因为 Ticket  CAS Server 产生的,因此,所以 CAS Server 的判断是毋庸置疑的。

       该协议完成了一个很简单的任务,就是 User(david.turing) 打开 IE ,直接访问 helloservice 应用,它被立即重定向到 CAS Server 进行认证, User 可能感觉到浏览器在 helloservcie  casserver 之间重定向,但 User 是看不到, CAS Client  CAS Server 相互间的 Service Ticket 核实 (Validation) 过程。当 CAS Server 告知 CAS Client 用户 Service Ticket 对应确凿身份, CAS Client 才会对当前 Request 的用户进行服务。

2.2.2 CAS 如何实现 SSO

       当我们的 Web 时代还处于初级阶段的时候, SSO 是通过共享 cookies 来实现,比如,下面三个域名要做 SSO 

http://www.blogjava.net

http://www.matrix.org.cn

http://www.csdn.net

如果通过 CAS 来集成这三个应用,那么,这三个域名都要做一些域名映射,

http://blogjava.cas.org

http://matrix.cas.org

http://csdn.cas.org

因为是同一个域,所以每个站点都能够共享基于 cas.org  cookies 。这种方法原始,不灵活而且有不少安全隐患,已经被抛弃了。

CAS 可以很简单的实现跨域的 SSO ,因为,单点被控制在 CAS Server ,用户最有价值的 TGC-Cookie 只是跟 CAS Server 相关, CAS Server 就只有一个,因此,解决了 cookies 不能跨域的问题。

回到 CAS 的基础协议图,当 Step3 完成之后, CAS Server 会向 User 发送一个 Ticket granting cookie (TGC)  User 的浏览器,这个 Cookie 就类似 Kerberos  TGT ,下次当用户被 Helloservice2 重定向到 CAS Server 的时候, CAS Server 会主动 Get 到这个 TGC cookie ,然后做下面的事情:

1,              如果 User 的持有 TGC 且其还没失效,那么就走基础协议图的 Step4 ,达到了 SSO 的效果。

2,              如果 TGC 失效,那么用户还是要重新认证 ( 走基础协议图的 Step3) 

2.2.2 CAS 的代理模式

       模式 1 已经能够满足大部分简单的 SSO 应用,现在,我们探讨一种更复杂的情况,即用户访问 helloservice  helloservice 又依赖于 helloservice2 来获取一些信息,如同:

User à helloservice à helloservice2

这种情况下,假设 helloservice2 也是需要对 User 进行身份验证才能访问,那么,为了不影响用户体验(过多的重定向导致 User  IE 窗口不停地 闪动 )  CAS 引入了一种 Proxy 认证机制,即 CAS Client 可以代理用户去访问其它 Web 应用。

代理的前提是需要 CAS Client 拥有用户的身份信息 ( 类似凭据 )  与其说之前我们提到的 TGC 是用户持有对自己身份信息的一种凭据,则这里的 PGT 就是 CAS Client 端持有的对用户身份信息的一种凭据。凭借 TGC User 可以免去输入密码以获取访问其它服务的 Service Ticket ,所以,这里,凭借 PGT  Web 应用可以代理用户去实现后端的认证,而无需前端用户的参与。

如下面的 CAS Proxy 图所示, CAS Client 在基础协议之上,提供了一个额外的 PGT URL  CAS Server, 于是, CAS Server 可以通过 PGT URL 提供一个 PGT  CAS Client 

cas_protocol-2.jpg 
       初学者可能会对上图的 PGT URL 感到迷惑,或者会问,为什么要这么麻烦,要通过一个额外的 URL( 而且是 SSL 的入口 ) 去传递 PGT ?如果直接在 Step 6 返回,则连用来做对应关系的 PGTIOU 都可以省掉。PGTIOU 设计是从安全性考虑的,非常必要, CAS 协议安全性问题我会在后面一节介绍。

于是, CAS Client 拿到了 PGT( PGTIOU-85…..ti2td ) ,这个 PGT  TGC 同样地关键, CAS Client 可以通过 PGT 向后端 Web 应用进行认证。如下图所示, Proxy 认证与普通的认证其实差别不大, Step1, 2 与基础模式的 Step 1,2 几乎一样,唯一不同的是, Proxy 模式用的是 PGT 而不是 TGC ,是 Proxy Ticket  PT )而不是 Service Ticket 

最终的结果是, helloservice2 明白 helloservice 所代理的客户是 David. Turing 同学,同时,根据本地策略, helloservice2 有义务为 PGTURL=http://helloservice/proxy 服务 (PGTURL 用于表示一个 Proxy 服务 ),于是它传递数据给 helloservice 。这样, helloservice 便完成一个代理者的角色,协助 User 返回他想要的数据。


cas_protocol-3.jpg 
   代理认证模式非常有用,它也是
 CAS 协议 v2 的一个最大的变化,这种模式非常适合在复杂的业务领域中应用 SSO 。因为,以前我们实施 SSO 的时候,都是假定以 IE User  SSO 的访问者,忽视了业务系统作为 SSO 的访问者角色。

2.3 CAS 安全性

       CAS 的安全性是一个非常重要的 Topic  CAS  v1  v3 ,都很依赖于 SSL ,它假定了这样一个事实,用户在一个非常不安全的网络环境中使用 SSO  Hacker  Sniffer 会很容易抓住所有的 Http Traffic ,包括通过 Http 传送的密码甚至 Ticket 票据。

2.3.1 TGC/PGT 安全性

       对于一个 CAS 用户来说,最重要是要保护它的 TGC ,如果 TGC 不慎被 CAS Server 以外的实体获得, Hacker 能够找到该 TGC ,然后冒充 CAS 用户访问所有授权资源。

       SSO 的安全性问题比普通应用的安全性还要严重,因为 SSO 存在一种门槛效应。以前即使 Hacker 能够截获用户在 Web 应用 A 的密码,它也未必能知道用户在 Web 应用 B 的密码,但 SSO  Hacker 只需要截获TGC( 突破了门槛 ) ,即能访问所有与该用户相关的所有应用系统。

       PGT  TGC 的角色是一样的,如果被 Hacker 获得,后果可想而知。

       从基础模式可以看出, TGC  CAS Server 通过 SSL 方式发送给终端用户,因此,要截取 TGC 难度非常大,从而确保 CAS 的安全性。

       因此,某些人认为 CAS 可以不使用 SSL 的想法需要更正一下, CAS 的传输安全性仅仅依赖与 SSL 

        Kerberos 一样 TGT  TGC 也有自己的存活周期。下面是 CAS  web.xml 中,通过 grantingTimeout 来设置 CAS TGC 存活周期的参数,参数默认是 120 分钟,在合适的范围内设置最小值,太短,会影响 SSO 体验,太长,会增加安全性风险。

    <context-param>

        <param-name>edu.yale.its.tp.cas.grantingTimeout</param-name>

        <param-value>7200</param-value>

    </context-param>

TGC 面临的风险主要并非传输窃取。比如你登陆了之后,没有 Logout ,离开了电脑,别人就可以打开你的浏览器,直接访问你授权访问的应用 ) ,设置一个 TGC 的有效期,可以减少被别人盗用,或者被 Hacker 入侵你的电脑直接获取你系统目录下的 TGC Cookie 

2.3.2 Service Ticket/Proxy Ticket 安全性

       首要明白, Service Ticket 是通过 Http 传送的,以为着所网络中的其他人可以 Sniffer 到其他人的 Ticket 

CAS 协议从几个方面让 Service Ticket 变得更加安全。

l         Service Ticket 只能使用一次。

CAS 协议规定,无论 Service Ticket 验证是否成功, CAS Server 都会将服务端的缓存中清除该 Ticket ,从而可以确保一个 Service Ticket 被使用两次。

l         Service Ticket 在一段时间内失效。

假设用户拿到 Service Ticket 之后,他请求 helloservice 的过程又被中断了, Service Ticket 就被空置了,事实上,此时, Service Ticket 仍然有效。 CAS 规定 Service Ticket 只能存活一定的时间,然后 CAS Server 会让它失效。通过在 web.xml 中配置下面的参数,可以让 Service Ticket 在多少秒内失效。

<context-param>

<param-name>edu.yale.its.tp.cas.serviceTimeout</param-name>

<param-value>300</param-value>

</context-param>

       该参数在业务应用的条件范围内,越小越安全。

l         Service Ticket 是基于随机数生成的。

Service Ticket 必须足够随机,如果 Service Ticket 生成规则被猜出(如果你使用了 ST+Helloservice+ 自增序列的方式, Hacker 就可以构造下一个 Ticket ), Hacker 就等于绕过 CAS 认证,直接访问所有服务。 

安全性:

用户只须在cas录入用户名和密码,之后通过ticket绑定用户,在cas客户端与cas校验是通过ticket,并不会在网上传输密码,所以可以保证安全性,密码不被窃取

原理:1个cookie+N个session

CAS创建cookie在所有应用中登录时cas使用,各应用通过在IE创建各自的session来标识应用是否已经登录。

Cookie:在cas为各应用登录时使用,实现了只须一次录入用户密码

Session:各应用会创建自己的session表示是否登录

登录

1.  CAS 登录时处理:

第一步:cas往浏览器增加cookie(TGC)

CAS向浏览器送回一个所谓的“内存cookie”。这种cookie并不是真的保存在内存中,而只是浏览器一关闭,cookie就自动过期。这个cookie称为“ticket-granting cookie”,用来表明用户已经成功地登录。

这个Cookie是一个加密的Cookie,其中保存了用户登录的信息。用于以后其它应用客户端登录。

第二步:cas同时创建一个ticket重定向到原来的cas客户端

认证成功后,CAS服务器创建一个很长的、随机生成的字符串,称为“Ticket”。随后,CAS将这个ticket和成功登录的用户,以及服务联系在一起。这个ticket是一次性使用的一种凭证,它只对登录成功的用户及其服务使用一次。使用过以后立刻失效。

2.  Cas 客户端应用的处理

第一步:收到ticket后,向cas提交验证ticket

Cas客户端收到ticket之后,应用程序需要验证ticket。这是通过将ticket 传递给一个校验URL来实现的。校验URL也是CAS服务器提供的。CAS通过校验路径获得了ticket之后,通过内部的数据库对其进行判断。如果判断是有效性,则返回一个NetID给应用程序。随后CAS将ticket作废,并且在客户端留下一个cookie。(谁来创建cookie?),

第二步:ticket验证后创建session

          以后登录此应用时,没有ticket,但IE能提供session,从session中取得CASReceipt,并验证如果有效说明已经在此应用认证过,允许访问此应用,

       到此为止,CAS会记录用户已在应用A已经登录

3.  用户登录到应用是如何处理

  用户进入应用B时,首先仍然会重定向到CAS服务器。不过此时CAS服务器不再要求用户输 入用户名和密码,而是首先自动寻找Cookie,根据Cookie中保存的信息,进行登录。然后,CAS同样给出新的ticket重定向应用B给cas验证(流程同应用A验证方式),如果验证成功则应用B创建session记录CASReceipt信息到session中,以后凭此session登录应用B。

到此为止,CAS会记录用户已在应用A和应用B进行登录,但是当用户在应用B退出cas登录时,要通知应用A进行退出,如何通知应用A呢?

      

登出

   

  CAS server接受请求后,会检测用户的TCG Cookie,把对应的session清除,同时会找到所有通过该TGC sso登录的应用服务器URL提交请求,所有的回调请求中,包含一个参数logoutRequest,内容格式如下:

< samlp:LogoutRequest  ID ="[RANDOM ID]"  Version ="2.0"  IssueInstant ="[CURRENT DATE/TIME]" >
< saml:NameID > @NOT_USED@ </ saml:NameID >
< samlp:SessionIndex > [SESSION IDENTIFIER] </ samlp:SessionIndex >
</ samlp:LogoutRequest >



所有收到请求的应用服务器application会解析这个参数,取得sessionId,根据这个Id取得session后,把session删除。
这样就实现单点登出的功能。

知道原理后,下面是结合源代码来讲述一下内部的代码怎么实现的。


客户端实现:

首先,要实现single sign out在 应用服务器application端的web.xml要加入以下配置

< filter >
   
< filter-name > CAS Single Sign Out Filter </ filter-name >
   
< filter-class > org.jasig.cas.client.session.SingleSignOutFilter </ filter-class >
</ filter >

< filter-mapping >
   
< filter-name > CAS Single Sign Out Filter </ filter-name >
   
< url-pattern > /* </ url-pattern >
</ filter-mapping >

< listener >
    
< listener-class > org.jasig.cas.client.session.SingleSignOutHttpSessionListener </ listener-class >
</ listener >


注:如果有配置CAS client Filter,则CAS Single Sign Out Filter 必须要放到CAS client Filter之前。

配置部分的目的是在CAS server回调所有的application进行单点登出操作的时候,需要这个filter来实现session清楚。

主要代码如下:
org.jasig.cas.client.session.SingleSignOutFilter

 1  public   void  doFilter( final  ServletRequest servletRequest,  final  ServletResponse servletResponse,  final  FilterChain      
 2 
 3  filterChain)  throws  IOException, ServletException {
 4           final  HttpServletRequest request  =  (HttpServletRequest) servletRequest;
 5 
 6           if  ( " POST " .equals(request.getMethod())) {
 7               final  String logoutRequest  =  request.getParameter( " logoutRequest " );
 8 
 9               if  (CommonUtils.isNotBlank(logoutRequest)) {
10 
11                   if  (log.isTraceEnabled()) {
12                      log.trace ( " Logout request=[ "   +  logoutRequest  +   " ] " );
13                  }
14                   // 从xml中解析 SessionIndex key值
15                   final  String sessionIdentifier  =  XmlUtils.getTextForElement(logoutRequest,  " SessionIndex " );
16 
17                   if  (CommonUtils.isNotBlank(sessionIdentifier)) {
18                           // 根据sessionId取得session对象
19                       final  HttpSession session  =  SESSION_MAPPING_STORAGE.removeSessionByMappingId(sessionIdentifier);
20 
21                       if  (session  !=   null ) {
22                          String sessionID  =  session.getId();
23 
24                           if  (log.isDebugEnabled()) {
25                              log.debug ( " Invalidating session [ "   +  sessionID  +   " ] for ST [ "   +  sessionIdentifier  +   " ] " );
26                          }
27                          
28                           try  {
29                   // 让session失效
30                              session.invalidate();
31                          }  catch  ( final  IllegalStateException e) {
32                              log.debug(e,e);
33                          }
34                      }
35                     return ;
36                  }
37              }
38          }  else  { // get方式 表示登录,把session对象放到SESSION_MAPPING_STORAGE(map对象中)
39               final  String artifact  =  request.getParameter( this .artifactParameterName);
40               final  HttpSession session  =  request.getSession();
41              
42               if  (log.isDebugEnabled()  &&  session  !=   null ) {
43                  log.debug( " Storing session identifier for  "   +  session.getId());
44              }
45               if  (CommonUtils.isNotBlank(artifact)) {
46                  SESSION_MAPPING_STORAGE.addSessionById(artifact, session);
47              }
48          }
49 
50          filterChain.doFilter(servletRequest, servletResponse);
51      }


SingleSignOutHttpSessionListener实现了javax.servlet.http.HttpSessionListener接口,用于监听session销毁事件

 1  public   final   class  SingleSignOutHttpSessionListener  implements  HttpSessionListener {
 2 
 3       private  Log log  =  LogFactory.getLog(getClass());
 4 
 5       private  SessionMappingStorage SESSION_MAPPING_STORAGE;
 6      
 7       public   void  sessionCreated( final  HttpSessionEvent event) {
 8           //  nothing to do at the moment
 9      }
10 
11       // session销毁时
12       public   void  sessionDestroyed( final  HttpSessionEvent event) {
13           if  (SESSION_MAPPING_STORAGE  ==   null ) { // 如果为空,创建一个sessionMappingStorage 对象
14              SESSION_MAPPING_STORAGE  =  getSessionMappingStorage();
15          }
16           final  HttpSession session  =  event.getSession(); // 取得当然要销毁的session对象
17          
18           if  (log.isDebugEnabled()) {
19              log.debug( " Removing HttpSession:  "   +  session.getId());
20          }
21           // 从SESSION_MAPPING_STORAGE map根据sessionId移去session对象
22          SESSION_MAPPING_STORAGE.removeBySessionById(session.getId());
23      }
24 
25       /**
26       * Obtains a { @link  SessionMappingStorage} object. Assumes this method will always return the same
27       * instance of the object.  It assumes this because it generally lazily calls the method.
28       * 
29       *  @return  the SessionMappingStorage
30        */
31       protected   static  SessionMappingStorage getSessionMappingStorage() {
32           return  SingleSignOutFilter.getSessionMappingStorage();
33      }
34  }



 服务器端实现


 已经登录的应用会在服务器端保存,所以服务端分别对各个应用发送http请求进行session清除操作。

网上参考资料

看了下面的浏览器cookie变化,会对cas有更深的理解

下载个httpwatch监控一下cookie的变化

客户端消息流程

1.       第一次访问http://localhost:8080/a,

CLIENT:没票据且SESSION中没有消息所以跳转至CAS

CAS:拿不到TGC故要求用户登录

  

2.       认证成功后回跳

CAS:通过TGT生成ST发给客户端,客户端保存TGC,并重定向到http://localhost:8080/a

CLIENT:带有票据所以不跳转只是后台发给CAS验证票据(浏览器中无法看到这一过程)

3.       第一次访问http://localhost:8080/b

CLIENT:没票据且SESSION中没有消息所以跳转至CAS

CAS:从客户端取出TGC,如果TGC有效则给用户ST并后台验证ST,从而SSO。【如果失效重登录或注销时,怎么通知其它系统更新SESSION信息呢??TicketGrantingTicketImpl类grantServiceTicket方法里this.services.put(id,service);可见CAS端已经记录了当前登录的子系统】

4.       再次访问http://localhost:8080/a

CLIENT:没票据但是SESSION中有消息故不跳转也不用发CAS验证票据,允许用户访问

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值