CAS 單點登出失效的問題(源碼跟蹤)

1、環境說明java

服務端:cas-server-3.5.2web

客戶端:cas-client-3.2.1+spring mvcspring

說明:服務端與客戶端均是走的Https數據庫

客戶端配置文件:spring-mvc

applicationContext-cas.xmlsession

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:sec="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">
    <!-- 讀取配置文件 -->
    <context:property-placeholder location="classpath*:cas.properties" ignore-unresolvable="true" />

    <bean name="singleSignOutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter" />

    <bean name="authenticationFilter" class="org.jasig.cas.client.authentication.AuthenticationFilter" p:renew="false"
        p:gateway="false" p:casServerLoginUrl="${cas.server.login.url}" p:serverName="${server.name}" />

    <bean name="ticketValidationFilter" class="org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter"
        p:redirectAfterValidation="true" p:serverName="${server.name}">
        <property name="ticketValidator">
            <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
                <constructor-arg index="0" value="${cas.server.url}" />
            </bean>
        </property>
    </bean>

    <bean name="httpServletRequestWrapperFilter" class="org.jasig.cas.client.util.HttpServletRequestWrapperFilter" />

    <bean name="assertionThreadLocalFilter" class="org.jasig.cas.client.util.AssertionThreadLocalFilter" />

</beans>

web.xmlmvc

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    metadata-complete="true">
    <display-name>Archetype Created Web Application</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath*:/applicationContext-cas.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- https://wiki.jasig.org/display/CASC/Configuring+Single+Sign+Out -->
    <!-- 用於單點退出,該過濾器用於實現單點登出功能,可選配置 The SingleSignOutFilter can affect character 
        encoding. -->
    <listener>
        <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
    </listener>

    <!-- Filter 定義 -->
    <!-- Character Encoding filter -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>
    
    <!-- Spring mvc -->
    <servlet>
        <servlet-name>springServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>cas oss info</servlet-name>
        <servlet-class>com.gqshao.cas.servlet.InfoServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>cas oss info</servlet-name>
        <url-pattern>/info</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>cas oss logout</servlet-name>
        <servlet-class>com.gqshao.cas.servlet.LogoutServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>cas oss logout</servlet-name>
        <url-pattern>/logout</url-pattern>
    </servlet-mapping>

    <!--1.用於單點退出 -->
    <filter>
        <filter-name>CAS Single Sign Out Filter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>singleSignOutFilter</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CAS Single Sign Out Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!--2.負責Ticket校驗 -->
    <filter>
        <filter-name>CAS Validation Filter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>ticketValidationFilter</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CAS Validation Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 3. 單點登陸驗證 -->
    <filter>
        <filter-name>CAS Authentication Filter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>authenticationFilter</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CAS Authentication Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!--4. CAS HttpServletRequest Wrapper Filter 這個是HttpServletRequet的包裹類,讓他支持getUserPrincipal,getRemoteUser方法來取得用戶信息 -->
    <filter>
        <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>httpServletRequestWrapperFilter</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!--5. CAS Assertion Thread Local Filter 這個類把Assertion信息放在ThreadLocal變量中,這樣應用程序不在web層也可以獲取到當前登陸信息 -->
    <filter>
        <filter-name>CAS Assertion Thread Local Filter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>assertionThreadLocalFilter</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CAS Assertion Thread Local Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

2、問題描述app

服務端與客戶端的部署從網上找了不少資料,總算是部署好了,數據庫中查詢用戶、單點登陸都沒有問題,在測試單點登出時發現老是不能進入CAS客戶端單點登陸的方法,dom

執行單點登出後 經過跟蹤源碼發現ide

 SingleSignOutFilter.java類dofilter方法中如下代碼塊始終不能進入

        if (handler.isTokenRequest(request)) {
            handler.recordSession(request);
        } else if (handler.isLogoutRequest(request)) {
            handler.destroySession(request);
            // Do not continue up filter chain
            return;
        } else {
            log.trace("Ignoring URI " + request.getRequestURI());
        }

        filterChain.doFilter(servletRequest, servletResponse);

因而排查服務端相關代碼

服務端登出邏輯主要是經過 LogoutController.java 類handleRequestInternal 方法中的代碼段

        if (ticketGrantingTicketId != null) {
            this.centralAuthenticationService
                .destroyTicketGrantingTicket(ticketGrantingTicketId);

            this.ticketGrantingTicketCookieGenerator.removeCookie(response);
            this.warnCookieGenerator.removeCookie(response);
        }

其中

centralAuthenticationService
                .destroyTicketGrantingTicket(ticketGrantingTicketId);

負責銷燬TGT,經過調用 ————————>CentralAuthenticationServiceImpl.java類中的destroyTicketGrantingTicket方法————————> ticket.expire();
見:

        Assert.notNull(ticketGrantingTicketId);

        if (log.isDebugEnabled()) {
            log.debug("Removing ticket [" + ticketGrantingTicketId + "] from registry.");
        }
        final TicketGrantingTicket ticket = (TicketGrantingTicket) this.ticketRegistry.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);

        if (ticket == null) {
            return;
        }

        if (log.isDebugEnabled()) {
            log.debug("Ticket found.  Expiring and then deleting.");
        }
        ticket.expire();
        this.ticketRegistry.deleteTicket(ticketGrantingTicketId);

——————>調用

AbstractWebApplicationService.java類中的 logOutOfService方法,
------------------主腳出現了
        if (this.httpClient != null) {
            return this.httpClient.sendMessageToEndPoint(getOriginalUrl(), logoutRequest, true);
        }

該處經過基於

HttpURLConnection

實現的HttpClient.java類向客戶端發送數據,HttpClient中主要代碼

        public Boolean call() throws Exception {
            HttpURLConnection connection = null;
            BufferedReader in = null;
            try {
                if (log.isDebugEnabled()) {
                    log.debug("Attempting to access " + url);
                }
                final URL logoutUrl = new URL(url);
                final String output = "logoutRequest=" + URLEncoder.encode(message, "UTF-8");

                connection = (HttpURLConnection) logoutUrl.openConnection();
                connection.setDoInput(true);
                connection.setDoOutput(true);
                connection.setRequestMethod("POST");
                connection.setReadTimeout(this.readTimeout);
                connection.setConnectTimeout(this.connectionTimeout);
                connection.setInstanceFollowRedirects(this.followRedirects);
                connection.setRequestProperty("Content-Length", Integer.toString(output.getBytes().length));
                connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                final DataOutputStream printout = new DataOutputStream(connection.getOutputStream());
                printout.writeBytes(output);
                printout.flush();
                printout.close();

                in = new BufferedReader(new InputStreamReader(connection.getInputStream()));

                while (in.readLine() != null) {
                    // nothing to do
                }

                if (log.isDebugEnabled()) {
                    log.debug("Finished sending message to" + url);
                }
                return true;
            } catch (final SocketTimeoutException e) {
                log.warn("Socket Timeout Detected while attempting to send message to [" + url + "].");
                return false;
            } catch (final Exception e) {
                log.warn("Error Sending message to url endpoint [" + url + "].  Error is [" + e.getMessage() + "]");
                return false;
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (final IOException e) {
                        // can't do anything
                    }
                }
                if (connection != null) {
                    connection.disconnect();
                }
            }
        }

 

並未針對啓用SSL的客戶端進行處理,故始終沒法向客戶端發送消息,爲了方便起見,直接對源碼進行修改來支持SSL的客戶端

修改的部分

if(url.startsWith("https:"))
                {
                    TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                            return null;
                        }

                        public void checkClientTrusted(
                                java.security.cert.X509Certificate[] certs, String authType) {
                        }

                        public void checkServerTrusted(
                                java.security.cert.X509Certificate[] certs, String authType) {
                        }
                    } };
                    httpsconnection = (HttpsURLConnection) logoutUrl.openConnection();
                    httpsconnection.setDefaultHostnameVerifier(new NullHostNameVerifier());
                    SSLContext sc = SSLContext.getInstance("SSL");
                    sc.init(null, trustAllCerts, new java.security.SecureRandom());
                    httpsconnection
                            .setDefaultSSLSocketFactory(sc.getSocketFactory());
                    httpsconnection.setDoInput(true);
                    httpsconnection.setDoOutput(true);
                    httpsconnection.setRequestMethod("POST");
                    httpsconnection.setReadTimeout(this.readTimeout);
                    httpsconnection.setConnectTimeout(this.connectionTimeout);
                    httpsconnection.setInstanceFollowRedirects(this.followRedirects);
                    httpsconnection.setRequestProperty("Content-Length", Integer.toString(output.getBytes().length));
                    httpsconnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                    final DataOutputStream printout = new DataOutputStream(httpsconnection.getOutputStream());
                    printout.writeBytes(output);
                    printout.flush();
                    printout.close();

                    in = new BufferedReader(new InputStreamReader(httpsconnection.getInputStream()));

                    while (in.readLine() != null) {
                        // nothing to do
                    }
                }
View Code

至此,解決了當下遇到的問題

整個過程走了很多彎路,能讀懂源碼,能正確經過DEBUG排除問題真的是一項須要持續鍛鍊的技能

在此mark一下

 

文中源碼閱讀部分感謝做者eg366

http://blog.csdn.net/eg366/article/details/14054949

相關文章
相關標籤/搜索