首页 > 其他分享 >SSO 之 (单点登录)实施中遇到的几个问题

SSO 之 (单点登录)实施中遇到的几个问题

时间:2023-09-20 14:06:25浏览次数:44  
标签:单点 登录 TGT 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单点退出实现机制。参见我的博客中 单点登录实现机制的【单点退出】部分。

      我们看一下源码,看到退出时调用类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);。。。

 

说明:

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>。。。

 

注:红字部分为相应配置内容扩展部分

      经过上述这样扩展,配置的排除路径作为请求时,单点登录过滤拦截就会忽略处理,实现了目标功能要求。

          不同应用服务,对请求的处理方式不同,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进行了扩展,思路也很简单,主要是将显示默认登录页面的调用,变成了远程调用业务子系统的页面地址,这个地址可以作为配置参数来配置。举例如下:

标签:单点,登录,TGT,SSO,超时,客户端
From: https://blog.51cto.com/u_16242566/7536790

相关文章

  • 如何上传专用密码和登录iCloud教程
    步骤1:在AppUploader这个页面中,依次按照要求操作。步骤2:进入管理您的AppleID页面,然后点击红圈中的更多。步骤3:根据步骤操作,即可生成专用密码。步骤4:在App Uploader页面输入前面生成的专用密码即可登录成功。步骤五:在iCloud页面进行登录操作。步骤六:找到AppID。 步骤七:输入App......
  • 直击火山引擎V-Tech峰会!仅需简单登录,即可极速体验数据引擎ByteHouse
    更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群 9月19日,火山引擎“数据飞轮·V-Tech数据驱动科技峰会”在上海举办。会上重磅发布数智平台VeDI应用大模型(LargeLanguageModels)能力,并进一步解读了数据飞轮的行业应用与实践。作为核......
  • 【.NET8】访问私有成员新姿势UnsafeAccessor(下)
    前言书接上回,我们讨论了在.NET8中新增的UnsafeAccessor,并且通过UnsafeAccessor访问了私有成员,这极大的方便了我们代码的编写,当然也聊到了它当前存在的一些局限性,那么它的性能到底如何?我们今天就来实际测试一下。测试代码话不多说,直接上代码,本次测试代码如下:usingSystem.Linq.......
  • redisson 工具类
    importcom.alibaba.fastjson.JSON;importcom.juxiao.xchat.dao.room.dto.CallStatus;importcom.juxiao.xchat.manager.cache.redis.RedissonManager;importlombok.extern.slf4j.Slf4j;importorg.redisson.api.*;importorg.redisson.client.protocol.ScoredEntry;i......
  • Redis学习之共享session(单点登录)
    介绍为什么需要共享session?防止多个后端服务器的数据存储不一致,导致用户访问时出现未登录的情况。如何实现共享session?使用独立的内存存储来存放session实现key如何设计?为了安全性,随机生成token,而不是拼接用户信息,防止恶意伪造或爆破。选择何种value数据结构存放用户信......
  • Spring Boot&Vue3前后端分离实战wiki知识库系统<十三>--单点登录开发二
    接着SpringBoot&Vue3前后端分离实战wiki知识库系统<十二>--用户管理&单点登录开发一继续往下。登录功能开发: 接下来则来开发用户的登录功能,先准备后端的接口。后端增加登录接口:1、UserLoginReq:先来准备用户登录的请求实体:packagecom.cexo.wiki.req;importjavax.validation.co......
  • 直播源码,zabbix忘记登录密码
    直播源码,zabbix忘记登录密码 1.首先登陆到数据库,选择zabbix数据库,查看Admin用户。 mysql>usezabbixReadingtableinformationforcompletionoftableandcolumnnamesYoucanturnoffthisfeaturetogetaquickerstartupwith-ADatabasechangedmysql>select......
  • KingbaseESV8R6用户登录失败自动锁定后解锁遇到权限问题
    测试用户登录失败自动锁定创建用户tee并授权。TEST=#createusertee;CREATEROLETEST=#alteruserteewithcreatedb;ALTERROLETEST=#TEST=#alteruserteewithcreaterole;ALTERROLETEST=#TEST=#alterroleteewithreplication;ALTERROLETEST=#alterus......
  • ORA-01017: 用户名/密码无效;登录被拒绝
    总结出现此错误的原因有多种:您的用户名或密码实际上不正确数据库配置不正确(tnanames.ora、$ORACLE_SID参数)现在,我们来看看这个错误的解决方案。ORA-01017解决方案有几种方法可以解决ORA-01017错误:检查用户名和密码是否正确这似乎是一个显而易见的步骤,但请确保您输入......
  • Springboot简单功能示例-4 自定义加密进行登录验证
    springboot-sample介绍springboot简单示例-自定义加密进行登录验证跳转到发行版软件架构(当前发行版)Springboot3.1.3hutoolbcprov-jdk18on安装教程gitclone--branch自定义加密进行登录验证[email protected]:simen_net/springboot-sample.git主要功能使用SM2库......