Several problems encountered in the implementation of SSO (single sign-on)

In the single sign-on application, the following problems are encountered: 1. Timeout problem; 2. jsessionid problem; 3. Sometimes the subsystem fails to exit normally during single-sign-out; 4. Some request paths do not require a single-sign-on filter Interception; 5. Different application service implementations may require the SSO client to be adapted. We analyze it in detail and propose solutions.

1. Timeout problem

        The CAS open source single sign-on SSO component we provide has two main deployment nodes: SSO server (deployed content is a web application), application system client (deployed content is cas client casclient.jar package and related configuration files) . Therefore, we analyze under what circumstances the timeout will occur according to the SSO mechanism. After multiple application systems are integrated with SSO, during the SSO single sign-on process, after successful login, the session of the application system client (the browser client is used as an example below) will save the authenticated user context, and the SSO server will generate a user The certificate ticket (TGT) is cached, and the browser client will save the TGC (TGT stored in the browser cookie). TGT is a certificate ticket used as a ticket (ST) for issuing SSO access services, and normal access can only be done after the ST ticket is issued. . The session of the browser client will time out (for example, the general web application client can set the session timeout value to 30 minutes or longer). After the timeout, the session will be invalidated and the user context will be cleared. TGC is still stored in the browser cookie. , it will only be cleared when the browser is closed. The timeouts on the SSO server are mainly TGT and ST timeouts. We generally set the timeout value TGT to 2 hours and ST to 5 minutes. Regarding the use of ST tickets, the parameters of the ticket are generally carried when accessing the service for the first time through SSO. After the ticket can be accessed normally after verifying the ticket, the SSO server will destroy the ST ticket and invalidate it. Regarding the use of TGT tickets, it is generally kept as Timeout time (2 hours), unless a single exit will destroy the TGT.

       Based on the above analysis, we can conclude that the SSO timeout mainly involves two elements: the browser's session timeout value and the TGT timeout value. Generally, the system sets the timeout value of TGT > the session timeout value of the browser, then there may be two kinds of timeouts: 1. TGT timeout (the browser session timeout value is small, and naturally it also times out); 2. The browser session timeout, TGT does not timeout .

      The first type is "1. TGT timeout". This process is very simple. The user's valid credentials and tickets are invalid. Naturally, to obtain a valid TGT ticket again, all you need to do is to jump to the login page and log in again.

      The second "2. The browser session times out, the TGT does not time out", at this time, the TGT ticket of the SSO server and the TGC (TGT in the cookie) of the browser client are still valid. When the browser client accesses the SSO again, it can carry the TGC (corresponding to the TGT of the server), and re-send the request for obtaining the ticket ST to the SSO server. After obtaining the ticket ST, it can normally access the application system with the valid ST ticket. This process is a communication interaction between the browser client and the SSO server. The user may not feel it, and there will be no interruption. It seems to be able to access continuously. This is to give the user a friendly access experience. When you understand this mechanism, you know that the SSO mechanism is actually working in the background.


2.jsessionid problem

      jsessionid is an identifier for the java client to maintain the session with the application server. Clients in other languages ​​(such as php) have other identifier keywords, and I don't know what they are. jsessionid generally exists in the browser cookie (this is usually executed automatically when the java client connects to the application server), generally it does not appear in the url, the server will take it out from the client's cookie, but if the browser disables If the cookie is set, the url needs to be rewritten, and the jsessionid is explicitly rewritten into the Url, so that the server can find the session id through this. The CAS open source single sign-on SSO component provides this mechanism. I studied the CAS source code and basically understood the processing mechanism of jsessionid. The general principle is as follows: when the user accesses the business system, the SSO client intercepts and redirects to the SSO server for authentication, the request path uri is written into ";jsessionid=specific session value", and the SSO server can distinguish this identification value from other The client requests are different, and authentication processing is performed. The returned response also sets the value of jsessionid to the client cookie. The reason why jsessionid is set in both the uri and the cookie is to ensure that the value of jsessionid can be set. Finally, after the single sign-on is successful, the returned business system access address also has the jsessionid parameter, which looks awkward in the uri address.

      2 solutions are provided, as follows:

     1) You can add the parameter "&method=POST" to the request address parameter of the login page address (remember that POST is required to be capitalized here), so that jsessionid will not be displayed in the final returned access uri.

     2) Modify the code as follows:

Add method cleanupUrl to class org.jasig.cas.util.UrlUtils

   

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);

    }

 

Modify the following line in the makeEntrySelection method of class org.jasig.cas.web.flow.DynamicRedirectViewSelector

 

default:

// return new ExternalRedirect(serviceResponse.getUrl());//Comment source code                                                                                                                                                                                                                                           

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

 

After running like this, the jsessionid in the url path does not exist.

3. When single-point exit, sometimes the subsystem fails to exit normally

      我们知道正常情况下,以用户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进行了扩展,思路也很简单,主要是将显示默认登录页面的调用,变成了远程调用业务子系统的页面地址,这个地址可以作为配置参数来配置。举例如下:

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326718823&siteId=291194637