zoukankan      html  css  js  c++  java
  • 在多点环境下使用cas实现单点登陆及登出

    CAS 介绍

    CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。CAS 具有以下特点:

    • 开源的企业级单点登录解决方案。
    • CAS Server 为需要独立部署的 Web 应用。
    • CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。

    CAS 原理和协议

    从结构上看,CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。下图 是 CAS 最基本的协议过程:

    在图中第3步用户认证成功后,cas server会生成Ticket Granting Ticket(票据授权票据,简称TGT),同时将TGT值以CASTGC为名保存到浏览器的cookie中,之后生成Service Ticket(服务票据,简称ST)并缓存,在第4步时将ST通过浏览器重定向的URL传给cas client。

    当cas client验证ST时,是在后台请求cas server验证ST,而cas server在已缓存的ST中查找是否存在cas client传来的ST,若存在则返回验证成功同时将该ST删除(这就保证了用同一ST不能反复进入client应用,同时这也是为什么不直接将TGT返回给cas client的原因)。

    那么名为CASTGC的cookie起什么作用呢?

    当客户访问另一个cas client时,同样会被重定向到cas server,而此时我们并不希望再次让用户输入用户密码登陆,名为CASTGC的cookie这时就体现出作用来了,cas server发现存在名为CASTGC的cookie就将其值在已保存的TGT中查找,若存在,则说明已存在合法的TGT,cas server就根据该TGT生成新的ST,接下来的流程就和以前一样了。

    CAS 协议中还提供了 Proxy (代理)模式,以适应更加高级、复杂的应用场景,具体介绍可以参考 CAS 官方网站上的相关文档。

    在多点环境下使用CAS

    虽然cas是作为开源单点登录解决方案的一个不错选择,但是官方提供的缺省实现代码却不并支持cas server多点部署以及每个cas client多点部署的情况。这在现今越来越强调服务稳定性的潮流下,多少显得有些不合时宜。那么是不是服务是多点部署就不能使用cas了呢?答案是否定的。

    多点部署cas server

    对cas server来说,默认实现不能支持多点部署的原因在于TGT保存时使用的ticket register类将TGT保存在了Java类的变量中。相关配置如下:

    WEB-INFspring-configuration icketRegistry.xml:

    <bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.DefaultTicketRegistry" />

    如果要支持多点部署,我们可以通过引入memcached的方式,在多点环境下仍然能够正常使用cas。我们可以新创建一个类(如MemcachedTicketRegistry)实现AbstractTicketRegistry接口,修改相关配置如下:

    WEB-INFspring-configuration icketRegistry.xml:(在此省略了memcachedClient的配置)

    <bean id="ticketRegistry" class="com.xxx.cas.server.MemcachedTicketRegistry">
        <property name="client" ref="memcachedClient" />
    </bean>

    这样cas server已经可以多点部署了,然而此时我们会发现单点登出功能不正常了。通过debug和查看代码,发现TGT中保存的service集合为空,这是单点登出不正常的直接原因,因为cas server会遍历TGT中保存的service集合,依次向对应的cas client发出退出请求。然而为什么TGT中保存的service集合会为空呢?这是因为TGT从第一次被保存到memcached后就再也没有被保存到memcached,这样从memcached中取得的TGT自然还是最初的TGT,当然其中的service会为空了,而cas默认实现中TGT是始终保持在内存中的自然不会有问题。既然找到了问题的原因就简单了,我们只要每当TGT增加service后,再次将TGT保存到memcached就能解决这个问题。

    WEB-INFspring-configurationapplicationContext.xml修改如下:

    <bean id="centralAuthenticationService" class="org.jasig.cas.CentralAuthenticationServiceImpl" ... />
    替换为
    <bean id="centralAuthenticationService" class="com.xxx.cas.server.CentralAuthenticationServiceImpl" ... />

    com.xxx.cas.server.CentralAuthenticationServiceImpl类相比org.jasig.cas.CentralAuthenticationServiceImpl类有如下不同:

    this.serviceTicketRegistry.addTicket(serviceTicket);
    //com.xxx.cas.server.CentralAuthenticationServiceImp类增加了下面一行:
    this.serviceTicketRegistry.addTicket(ticketGrantingTicket);

    多点部署cas client

    对cas client来说,默认实现不能支持多点部署的原因在于cas client使用了session来保存凭证,要知道多点部署的应用其session是各自独立的,即使通过配置实现了session的同步,性能也会很差。那么如何解决这个问题呢?答案还是memcached。

    我们可以用过滤器filter将自定义的request传递给后面的调用,filter内容如下:

    SnaHttpServletRequest request = new SnaHttpServletRequest((HttpServletRequest) req,(HttpServletResponse) res, client);
    WebContext instance = new WebContext(request, (HttpServletResponse) res, ctx);
    try {
        WebContext.set(instance);
        chain.doFilter(request, res);
    } finally {
        request.save();
        WebContext.set(null);
    }

    其中SnaHttpServletRequest类继承自HttpServletRequestWrapper类,覆盖HttpSession getSession()和HttpSession getSession(boolean create)方法,使这两个方法返回实现HttpSession接口但是以memcached为核心实现的类(如MemcachedSession),而MemcachedSession类必定是以cookie为依据的,否则无法返回正确的数据。

    这其中代码的具体实现,网上已经有不少,这里就不展示了。

    cas client经过这样的改动后,在多点部署下可以单点登陆了,但单点登出仍有问题。原因是因为SingleSignOutFilter将ticketId和session对应关系用HashMap来保存,在多点环境下自然不能正常工作。我们可以将ticketId和cookie值的对应关系保存在memcached上,从而实现单点登出。修改办法如下:

    SingleSignOutFilter类:

    public void init(final FilterConfig filterConfig) throws ServletException {
        String memcachedId = "memcachedClient";
        this.client = (XMemcachedClient) WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext()).getBean(memcachedId);
        handler.setSessionMappingStorage(new MemcachedBackedSessionMappingStorage(client));
    //红色部分是新增的内容
    if (!isIgnoreInitConfiguration()) { handler.setArtifactParameterName(getPropertyFromInitParams(filterConfig, "artifactParameterName", "ticket")); handler.setLogoutParameterName(getPropertyFromInitParams(filterConfig, "logoutParameterName", "logoutRequest")); } handler.init(); }

    MemcachedBackedSessionMappingStorage类继承自SessionMappingStorage接口,代码如下:

    public final class MemcachedBackedSessionMappingStorage implements SessionMappingStorage {
        private XMemcachedClient client;
        private static final int TIMEOUT = 60 * 60 * 24;
    
        private final String MANAGED_SESSIONS = "MANAGED_SESSIONS.";
        private final String ID_TO_SESSION_KEY_MAPPING = "ID_TO_SESSION_KEY_MAPPING.";
    
        private final Log log = LogFactory.getLog(getClass());
    
        public MemcachedBackedSessionMappingStorage(XMemcachedClient client) {
            this.client = client;
        }
    
        public synchronized void addSessionById(String mappingId, HttpSession session) {
            try {
                client.set(ID_TO_SESSION_KEY_MAPPING + session.getId(), TIMEOUT, mappingId);
                client.set(MANAGED_SESSIONS + mappingId, TIMEOUT, session.getId());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
    
        }
    
        public synchronized void removeBySessionById(String sessionId) {
            if (log.isDebugEnabled()) {
                log.debug("Attempting to remove Session=[" + sessionId + "]");
            }
    
            try {
                final String key = client.get(ID_TO_SESSION_KEY_MAPPING + sessionId);
    
                if (log.isDebugEnabled()) {
                    if (key != null) {
                        log.debug("Found mapping for session.  Session Removed.");
                    } else {
                        log.debug("No mapping for session found.  Ignoring.");
                    }
                }
                client.delete(MANAGED_SESSIONS + key);
                client.delete(ID_TO_SESSION_KEY_MAPPING + sessionId);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        public synchronized HttpSession removeSessionByMappingId(String mappingId) {
            HttpSession session = null;
            try {
                String sessionId = client.get(MANAGED_SESSIONS + mappingId);
                session = new MemcachedSession(client, sessionId, false);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
    
            if (session != null) {
                removeBySessionById(session.getId());
            }
    
            return session;
        }
    }

    其它修改 

    在CAS的默认实现中,所有与 CAS 的交互均采用 SSL 协议,确保ST 和 TGC(Ticket Granted Cookie,对应TGT的名为CASTGC的cookie) 的安全性。这虽然极大保证了安全性,但在某些情况下,并不想使用SSL协议,那么可以进行如下修改:

    WEB-INFspring-configuration icketGrantingTicketCookieGenerator.xml:

    <bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
            p:cookieSecure="true"
            p:cookieMaxAge="-1"
            p:cookieName="CASTGC"
            p:cookiePath="/cas" />
    修改为:
    <bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
            p:cookieSecure="false"
            p:cookieMaxAge="-1"
            p:cookieName="CASTGC"
            p:cookiePath="/cas" />

    WEB-INFspring-configurationwarnCookieGenerator.xml:

    <bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
            p:cookieSecure="true"
            p:cookieMaxAge="-1"
            p:cookieName="CASPRIVACY"
            p:cookiePath="/cas" />
    修改为:
    <bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
            p:cookieSecure="false"
            p:cookieMaxAge="-1"
            p:cookieName="CASPRIVACY"
            p:cookiePath="/cas" />

    WEB-INFdeployerConfigContext.xml:

    <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
                        p:httpClient-ref="httpClient" />
    修改为:
    <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
                        p:httpClient-ref="httpClient" p:requireSecure="false"/>

    进行上面的修改后cas就能支持普通http协议的单点登录了。

  • 相关阅读:
    java正则表达式语法详解及其使用代码实例 (转)
    【SpringMVC学习09】SpringMVC与前台的json数据交互 (转)
    SpringMVC基于代码的配置方式(零配置,无web.xml)
    倒车入库操作要求
    R通过RJDBC连接外部数据库 (转)
    卡尔曼滤波——11.预测峰值
    卡尔曼滤波——10.均值漂移
    卡尔曼滤波——6.评估高斯分布
    神经网络入门——16实现一个反向传播
    神经网络入门——15反向传播
  • 原文地址:https://www.cnblogs.com/huangbin/p/3282643.html
Copyright © 2011-2022 走看看