1. 程式人生 > >cas的客戶端應用是負載均衡,單點退出怎麼辦?

cas的客戶端應用是負載均衡,單點退出怎麼辦?

之前的專案一直是單節點,這次在生產系統中使用了負載均衡,一個應用部署了兩個節點,負載均衡策略未知。這樣在使用時發現了這麼一個問題:在單點退出後,應用有時候可以退出,但有時還在登陸狀態,這就很鬱悶了。

我的cas版本是5.1.2。一點點排查,把每個節點的日誌都開啟,把日誌級別設定成最小trace,功夫不負有心人,發現了問題:使用者在瀏覽器訪問的都是節點一,在cas單點退出後,cas服務端傳送logoutRequest的post請求,結果負載均衡把請求傳送到了節點二,節點二壓根未訪問過,logoutRequest請求當然不起作用。

問題找到了,看官可能覺得樓主有點low,首先,負載均衡具體什麼策略,為什麼會發錯節點;第二,負載均衡多節點為什麼不用session共享。下面我一一解答。

第一個,負載均衡不是我負責的,我只知道通過瀏覽器訪問一個應用能保證每次都訪問同個節點,這就保證了使用者每次都攜帶登入資訊。我以為採用的是nginx的ip的hash值,結果被告知不是,是通過硬體控制負載均衡的。

第二個,之前公司專案差不多都是單節點,幾乎所有的應用都未使用session共享,況且現在是上線的關鍵節點,現改也來不及,最好是修改cas客戶端的東西,大家都更新一下jar包就可以了。

        logger.trace("Logout request:\n{}", logoutMessage);
        final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
        if (CommonUtils.isNotBlank(token)) {
            final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);

            if (session != null) {
                final String sessionID = session.getId();
                logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);

                try {
                    session.invalidate();
                } catch (final IllegalStateException e) {
                    logger.debug("Error invalidating session.", e);
                }
                this.logoutStrategy.logout(request);
            }

瞭解cas的同學知道,cas服務端傳送logoutRequest的post請求,裡面攜帶token資訊,客戶端就憑藉這個token找到session。圖中標紅部分,當未找到時不起作用,解決之道就在此,我們拿到了token資訊,然後向攜帶著token嚮應用的各個節點發送一次退出請求,這樣就能找到session,然後失效退出了。

就這樣,問題找到了,就著手解決吧。

第一步:修改cas客戶端,當session為空時,傳送post請求,上程式碼。

        logger.trace("Logout request:\n{}", logoutMessage);
        final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
        if (CommonUtils.isNotBlank(token)) {
            final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);

            if (session != null) {
                final String sessionID = session.getId();
                logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);

                try {
                    session.invalidate();
                } catch (final IllegalStateException e) {
                    logger.debug("Error invalidating session.", e);
                }
                this.logoutStrategy.logout(request);
            }else{
                if(CommonUtils.isNotBlank(this.balanceUrls)) {
                    String[] urls = balanceUrls.split(",");
                    for(String url : urls) {
                        Map<String, Object> map = new HashMap<String, Object>();
                        map.put("token",token);
                        try {
                //傳送post請求,不要在意我的工具類名字,複製來的 HttpClient.GET_JCMH_QX(map, url + "/logoutRequestOther"); }catch(Exception e) { logger.debug("Error HttpClient ", e); } } } }

比之前的程式碼多了else語句,這裡的balanceUrls(在filter的initParam維護),是負載均衡的轉發地址,多個用,隔開,然後遍歷傳送post請求,請求路徑統一為“/logoutRequestOther“。

第二步在我們第一步的/logoutRequestOthe請求肯定是請求不到的,我們怎麼處理呢?為了最小的改動,在cas客戶端增加filter進行處理,增加LogoutForBalanceFilter類。

 

 

 一定要跟SingleSignOutFilter同目錄,原因是我們需要用到 private SessionMappingStorage sessionMappingStorage ,session資訊就儲存在這個類中。

package org.jasig.cas.client.session;

import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.JsonUtil;
import org.jasig.cas.client.util.XmlUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.util.Map;

public class LogoutForBalanceFilter implements Filter {

    private final Logger logger = LoggerFactory.getLogger(LogoutForBalanceFilter.class);

    private SessionMappingStorage sessionMappingStorage;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
            FilterChain chain) throws IOException, ServletException {

        if (servletRequest instanceof HttpServletRequest) {
            HttpServletRequest req = (HttpServletRequest) servletRequest;
            String uri = req.getRequestURI();
            logger.debug("================== uri [{}]", uri);
            if (uri.contains("/logoutRequestOther")) {
                if (sessionMappingStorage == null) {
                    sessionMappingStorage = getSessionMappingStorage();
                }
         //獲取post請求的引數 Map<?,?> map = JsonUtil.jsonStrToMap(getParams(req)); if(map != null) { String token = (String)map.get("token"); logger.debug("================== token [{}]", token); if (CommonUtils.isNotBlank(token)) {
              //通過token查詢session final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token); if (session != null) { final String sessionID = session.getId(); logger.debug("Invalidating session [{}] for token [{}]", sessionID, token); try { session.invalidate(); } catch (final IllegalStateException e) { logger.debug("Error invalidating session.", e); } //this.logoutStrategy.logout(request); } } } return; } chain.doFilter(servletRequest, servletResponse); }else { chain.doFilter(servletRequest, servletResponse); } } @Override public void destroy() { } protected static SessionMappingStorage getSessionMappingStorage() { return SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage(); } protected String getParams(HttpServletRequest request){ String body = ""; StringBuilder stringBuilder = new StringBuilder(); BufferedReader bufferedReader = null; InputStream inputStream = null; try { inputStream = request.getInputStream(); if (inputStream != null) { bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); char[] charBuffer = new char[128]; int bytesRead = -1; while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { stringBuilder.append(charBuffer, 0, bytesRead); } } else { stringBuilder.append(""); } } catch (IOException ex) { ex.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (bufferedReader != null) { try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } body = stringBuilder.toString(); return body; } }

至此客戶端更改完畢。

這樣,同事們只需要更改下web.xml,增加一個filter,新增負載均衡轉發的地址引數列表。

    <filter>

        <filter-name>Cas_Balance_Logout</filter-name>

        <filter-class>org.jasig.cas.client.session.LogoutForBalanceFilter</filter-class>

    </filter>

    <filter-mapping>

        <filter-name>Cas_Balance_Logout</filter-name>

        <url-pattern>/logoutRequestOther</url-pattern>

    </filter-mapping>
    
    <filter>

<filter-name>CAS Single Sign Out Filter</filter-name> <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>http://zsptsso.jlsw.tax.cn/cas</param-value> </init-param> <init-param> <param-name>balanceUrls</param-name> <param-value>http://123.14.62.125:8002/zntx,http://123.14.62.126:8002/zntx</param-value> </init-param> </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>

圖中紅色部分就是同事們需要增加的,是不是很簡單呢,完