SSO(單點登錄)實施中遇到的幾個問題

       單點登錄應用中,遇到如下的幾個問題:1.超時問題;2.jsessionid問題;3.單點退出時有時子系統未能正常退出;4.有些請求路徑不需要單點登錄過濾器攔截;5.不同應用服務實現可能要求SSO客戶端做適應性改造。我們具體分析一下,並提出解決方法。

1.超時問題

        我們提供的CAS開源單點登錄SSO組件,它部署節點主要有2個:SSO服務器(部署內容爲一個web應用)、應用系統客戶端(部署內容爲cas客戶端casclient.jar包和相關配置文件)。因此我們根據SSO機制分析一下什麼情況下會出現超時。多個應用系統進行SSO集成後,SSO單點登錄過程中,登錄成功後,應用系統客戶端(以下用瀏覽器客戶端爲例)的session會保存認證後的用戶上下文,SSO服務器會生成一個用戶憑證票據(TGT)並緩存起來,瀏覽器客戶端會保存TGC(瀏覽器cookie中存儲的TGT),TGT是作爲發放SSO訪問服務的票據(ST)的一個憑證票據,發放ST票據後才能正常訪問。而瀏覽器客戶端的session會超時(如一般web應用客戶端可以設置session的timeout值爲30分鐘或更長),超時後會讓session失效,清空用戶上下文,TGC因爲仍然是保存在瀏覽器cookie中,只有關閉瀏覽器纔會清除。SSO服務器端的超時主要是TGT、ST超時,我們一般會設置超時值TGT爲2小時,ST爲5分鐘。關於ST票據使用,一般在首次SSO訪問服務時攜帶着該票據參數,驗證票據後能正常訪問後,SSO服務器就將此ST銷燬失效了;關於TGT票據的使用,一般是正常訪問時一直保持爲超時時間(2小時),除非做單點退出會銷燬TGT。

       基於以上分析,我們可以得出結論,SSO的超時主要涉及2個要素:瀏覽器的session超時值、TGT的超時值。一般系統設置TGT的超時值>瀏覽器的session超時值,那麼可能有2種超時情況:1.TGT超時(瀏覽器session超時值小,自然也超時);2.瀏覽器session超時,TGT不超時。

      第一種“1.TGT超時”,這個處理很簡單,用戶的有效憑證票據都失效了,自然要重新取得有效憑證票據TGT,需要做的就是重新跳轉到登錄頁面重新登錄。

      第二種”2.瀏覽器session超時,TGT不超時“,這時SSO服務器的TGT票據,以及瀏覽器客戶端的TGC(cookie中的TGT)仍然有效。瀏覽器客戶端再次SSO訪問時就可以攜帶TGC(與服務器的TGT對應),向SSO服務器重新發送取得票據ST請求,取得票據ST後,攜帶着有效ST票據可以正常訪問應用系統了。這個過程是瀏覽器客戶端與SSO服務器的一個通訊交互,用戶可能感覺不到,不會出現中斷,好像能連續訪問,這是爲了給用戶一個友好的訪問體驗。明白這個機制,就知道實際上是SSO機制在後臺起作用了。


2.jsessionid問題

      jsessionid是java客戶端與應用服務器維持session的一個標識,其他語言客戶端(如php)有其他標識關鍵字,具體是什麼還不太瞭解。jsessionid一般存在於瀏覽器cookie中的(這個一般java客戶端連接到應用服務器會自動執行的),一般情況下不會出現在url中,服務器會從客戶端的cookie中取出來,但是如果瀏覽器禁用了cookie的話,就要重寫url了,顯式的將jsessionid重寫到Url中,方便服務器來通過這個找到session的id。CAS開源單點登錄SSO組件就提供了這個機制。我研究了CAS源碼,基本明白了jsessionid的處理機制。大致原理如下:用戶訪問業務系統,SSO客戶端攔截,重定向到SSO服務器認證時,就將請求路徑uri中寫入";jsessionid=具體的session值",SSO服務器可以分辨出這個標識值與其他客戶端請求不同,進行認證處理,返回的響應給客戶端cookie同時也設置了jsessionid的值,之所以在uri和cookie中都設置了jsessionid,是爲了雙重保障能設置jsessionid值。最後單點登錄成功後,返回業務系統訪問地址也帶有jsessionid參數,這個在uri地址中看起來很彆扭。

      提供2種解決方法,如下:

     1) 可以在登錄頁面地址的請求地址參數中加入參數”&method=POST“(記住這裏要求POST大寫),這樣就可以在最後返回的訪問uri中不顯示jsessionid。

     2)修改代碼如下:

類org.jasig.cas.util.UrlUtils中增加方法cleanupUrl

   

public static final String cleanupUrl(final String url) {                                                                                                                                                         

        if (url == null) {

            return null;

        }

         final int jsessionPosition = url.indexOf(";jsession");

         if (jsessionPosition == -1) {

            return url;

        }

         final int questionMarkPosition = url.indexOf("?");

         if (questionMarkPosition < jsessionPosition) {

            return url.substring(0, url.indexOf(";jsession"));

        }

         return url.substring(0, jsessionPosition)

            + url.substring(questionMarkPosition);

    }

類org.jasig.cas.web.flow.DynamicRedirectViewSelector的makeEntrySelection方法中修改如下行

default:

//  return new ExternalRedirect(serviceResponse.getUrl());//註釋源碼                                                                                                                                                                                                                                           

     return new ExternalRedirect(UrlUtils.cleanupUrl(serviceResponse.getUrl()));//清除url中jsessionid 

這樣運行後,url路徑中的jsessionid就不存在了。

3.單點退出時有時子系統未能正常退出

      我們知道正常情況下,以用戶A單點登錄系統,正常訪問各子系統,然後執行單點退出時,退出成功後一般跳轉回到登錄頁面要求重新登錄,這時各已登錄的子系統session被銷燬,再次以另一個用戶B登錄進入後,各子系統顯示的應當是用戶B的數據信息。可是有時卻發現有些子系統仍然顯示的是用戶A的數據信息,這屬於偶發現象。現在分析一下產生這種情況可能的原因,找出解決辦法。

      若想了解單點退出機制原理,我們可以先看看CAS源碼的SSO單點退出實現機制。參見我的博客中 http://blog.csdn.net/yan_dk/article/details/7095091單點登錄實現機制的【單點退出】部分。

      我們看一下源碼,看到退出時調用類AbstractWebApplicationService的方法logOutOfService(),代碼如下

public synchronized boolean logOutOfService(final String sessionIdentifier) {
        if (this.loggedOutAlready) {
            return true;
        }
     
        LOG.debug("Sending logout request for: " + getId());

        final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\""
            + GENERATOR.getNewTicketId("LR")
            + "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime()
            + "\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>"
            + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";

        HttpURLConnection connection = null;

        try {
            final URL logoutUrl = new URL(getOriginalUrl());
            final String output = "logoutRequest=" + URLEncoder.encode(logoutRequest, "UTF-8");

            connection = (HttpURLConnection) logoutUrl.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            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();

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

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

            return true;
        } catch (final Exception e) {
            return false;
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
            this.loggedOutAlready = true;
        }
    }


我們看到紅字部分,在調用發生異常時,代碼是直接返回false的,也就是說單點退出發生錯誤時,SSO服務器並沒有做異常處理,直接返回,這樣就有可能在出現異常時(比如網絡瞬時閃斷),雖然系統界面退出後返回登錄頁面,但是SSO服務器並沒有退出處理,沒有銷燬登錄會話,所以就可能出現沒有真正退出,仍然顯示前一用戶的會話信息。這個應該是CAS源碼的一個bug,解決方法是在此處積累錯誤日誌,並拋出異常處理。這樣應該能解決此問題。修改代碼如下:

。。。                                                                                                                                                                                                                               

} catch (final Exception e) {
         LOG.error("--------------Sending logout request for URL: " + getOriginalUrl()+"Network connection failed.");
           throw new Exception(e);
        } finally {

。。。

說明:

4.有些請求路徑不需要單點登錄過濾器攔截

       業務系統web應用在使用單點登錄組件時,有些請求路徑不需要單點登錄過濾器攔截,比如公共開放的路徑,不需要認證都可以自由訪問的路徑,單點登錄過濾器配置的映射路徑一般以通配符匹配路徑,但要把這些路徑單獨提取出來,讓過濾器不攔截做單點登錄處理,就需要對原有過濾器進行擴展改造,才能實現這個功能。

       擴展實現代碼如下:

public class CASFilter implements Filter {  

public static enum ResponseType {
        BREAK, GOON, RETURN
    }                    
                                                                                                                                                          

...

public void doFilter(
        ServletRequest request,
        ServletResponse response,
        FilterChain fc){

。。。

CASReceipt receipt = (CASReceipt) session.getAttribute(CAS_FILTER_RECEIPT);

if (receipt != null && isReceiptAcceptable(receipt)) {
           log.trace("CAS_FILTER_RECEIPT attribute was present and acceptable - passing  request through filter..");
             fc.doFilter(request, response);
             return;
         }else{
             responeType = beforeDoSSOFilter(request, response);
             if(ResponseType.RETURN==responeType){
              return ;
             }else if(ResponseType.BREAK==responeType) {
                 fc.doFilter(request, response);
                 return;
             }  //else go on
         }

}  

//過濾器的前置處理

public ResponseType beforeDoSSOFilter(ServletRequest request,
   ServletResponse response) {
  return ResponseType.GOON;

 }

}

 

注:主要看原CASFilter 類紅字部分擴展代碼。

擴展實現類BMCASFilter

package com.sitechasia.sso.bmext;

public class BMCASFilter extends CASFilter {                                                                                                                                                                                                                                                
 private final Log log = LogFactory.getLog(this.getClass());
    private static String ssoclient_passedPathSet;//設置不被sso過濾器攔截的請求路徑,需要符合url路徑通配符,多個路徑可以","分割
 public static final String PASSEDPATHSET_INIT_PARAM="passedPathSet";//web.xml配置文件中的參數
 @Override
 public void init(FilterConfig config) throws ServletException {
  super.init(config);
  ssoclient_passedPathSet = SSOClientPropertiesSingleton.getInstance().getProperty(ClientConstants.SSOCLIENT_PASSEDPATHSET)==null?config.getInitParameter(PASSEDPATHSET_INIT_PARAM):SSOClientPropertiesSingleton.getInstance().getProperty(ClientConstants.SSOCLIENT_PASSEDPATHSET);
 }
   
    @Override
 public ResponseType beforeDoSSOFilter(ServletRequest request,
   ServletResponse response) {
    if (ssoclient_passedPathSet != null) {//路徑過濾
     HttpServletRequest httpRequest =(HttpServletRequest)request;
           String requestPath = httpRequest.getRequestURI();
//           String ls_requestPath = UrlUtils.buildFullRequestUrl(httpRequest.getScheme(), httpRequest.getServerName(), httpRequest.getServerPort(), requestPath, null);
          
        PathMatcher  matcher = new AntPathMatcher();
        String passedPaths[]=null;
        passedPaths =ssoclient_passedPathSet.split(",");
        
        if(passedPaths!=null){
         boolean flag;
         for (String passedPath : passedPaths) {
          flag = matcher.match(passedPath, requestPath);//ls_requestPath
                if(flag){
                      log.info("sso client request path '"+requestPath+"'is matched,filter chain will be continued.");
                  return ResponseType.BREAK;
                }
      }
        }
    }
  return ResponseType.GOON;
 }

 
}

web.xml文件中配置修改如下:

<filter>
  <description>單點登陸請求過濾器</description>
  <filter-name>CASFilter</filter-name>
  <filter-class>com.sitechasia.sso.dmext.filter.DMCASFilter</filter-class>

...

<init-param>
   <description>排除路徑</description>
   <param-name>passedPathSet</param-name>
   <param-value>
    /**/restful/userLogin/findPassword,
    /**/restful/userLogin/findIllegalLoginCount,
    /**/restful/tenantManager/**,
    /**/restful/lock/**,
    /**/restful/export/**
   </param-value>
  </init-param>

 </filter>

<filter-mapping>
        <filter-name>CASFilter</filter-name>
        <url-pattern>/index.jsp</url-pattern>
</filter-mapping>

。。。

注:紅字部分爲相應配置內容擴展部分

      經過上述這樣擴展,配置的排除路徑作爲請求時,單點登錄過濾攔截就會忽略處理,實現了目標功能要求。

 5.不同應用服務實現要求SSO客戶端做適應性改造

          不同應用服務,對請求的處理方式不同,SSO客戶端常規方法不能處理的需要進行適應性改造。我們先看一下SSO客戶端常規方法處理請求,web應用中定義一個過濾器casfilter,並配置對安全資源攔截,首次登錄系統、或者訪問超時時,安全資源的請求被過濾器casfilter攔截,將安全資源路徑包裝爲service參數,並重定向(http狀態爲302)到SSO服務器地址進行認證,輸入憑證信息(用戶名、密碼等),SSO服務器經過認證、驗證票據機制處理,認證成功後,登錄通過後繼續訪問安全資源,再次訪問安全資源時,過濾器casfilter攔截,根據用戶上下文判斷用戶已經登錄,就不再攔截安全資源,直接正常訪問安全資源。而有些應用中使用其他方式處理請求,按上述常規方法不好處理,下面舉實例來看看解決方案。

5.1.Ajax客戶端

         公司有一個應用系統使用Ajax客戶端來處理請求並取得響應,在訪問超時時,Ajax客戶端請求由於使用異步處理技術(只局部刷新頁面),不能將請求繼續302重定向到SSO服務器重新認證處理,不能繼續訪問安全資源,界面響應處於一直延遲狀態,很不友好。我們針對此應用進行適應性改造。改造要點如下:

  • 我們約定應用系統的一個Ajax請求的標記(如ajaxRequestFlag=ajaxRequest),在安全資源ajax調用請求時攜帶此請求參數,還約定一個超時請求的響應狀態值(如555),SSO客戶端的過濾器casfilter的相應類程序對此約定進行了相應的改造。
  • 應用系統的安全資源頁面中引入文件(HttpStatusSSO.js)SSO對Axax請求的http狀態的處理,其中有超時時的狀態處理、判斷是否登錄、驗證票據等的處理,都是通過ajax請求方式來處理的,其中依賴一個驗證票據的資源ticketValidate.jsp。

經過上述改造後,應用系統的安全資源發送攜帶ajax請求標記的請求,在超時請求時,SSO客戶端返回約定響應碼(555),HttpStatusSSO.js文件判斷處理後轉發給相應的登錄頁面重新認證。單點登錄的訪問也能夠正常的實現功能。這樣只擴展了SSO客戶端組件的實現,向下版本兼容,保證了SSO組件的統一完整性。

上述改造方式是通過實踐開發出來的,可能還有更好的方法來改造擴展,今後持續改進吧。

 後記:現在官方的CAS源碼3.4版本已經支持Ajax請求的客戶端,它的實現機制有待於進一步的研究,大家可以參考。

5.2.多域認證

         有時我們遇到,不同子系統的認證實現的數據源可能來自一個用戶數據庫。我公司就是這種情況,採用Saas模式運營的軟件,認證的用戶來源於不同的租戶或者域,基於這樣的情況,我們對SSO進行了擴展,主要提供了認證接口,認證實現上,可以在認證時動態取得租戶對應的數據源,對用戶進行認證處理。舉例如下:

 

5.3.SSO集中認證登錄頁面需要在業務子系統中定製

         SSO集中認證的登錄頁面默認是放在SSO服務器端的,樣式也很不好看,用戶需要把登錄頁面放在業務子系統中自行定製,我們對SSO進行了擴展,思路也很簡單,主要是將顯示默認登錄頁面的調用,變成了遠程調用業務子系統的頁面地址,這個地址可以作爲配置參數來配置。舉例如下:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章