前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CAS之单点登出逻辑详解

CAS之单点登出逻辑详解

作者头像
tunsuy
发布2022-10-27 09:54:23
2.3K0
发布2022-10-27 09:54:23
举报
文章被收录于专栏:有文化的技术人

单点登出功能跟单点登录功能是相对应的,旨在通过Cas Server的登出使所有的Cas Client都登出。Cas Server的登出是通过请求“/logout”发生的,即如果你的Cas Server部署的访问路径为“https://localhost:8443/cas”时,通过访问“https://localhost:8443/cas/logout”可以触发CasServer的登出操作,进而触发Cas Client的登出。在请求Cas Server的logout时,Cas Server会将客户端携带的TGC删除,同时回调该TGT对应的所有service,即所有的Cas Client。Cas Client如果需要响应该回调,进而在Cas Client端进行登出操作的话就需要有对应的支持。

配置文件

具体来说,需要在Cas Client应用的web.xml文件中添加如下Filter和Listener。

代码语言:javascript
复制
<filter>
   <filter-name>CAS Single Sign Out Filter</filter-name>
   <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</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>

当然,也可以自定义filter和监听,通过该继承或者组合SingleSignOutFilterSingleSignOutHttpSessionListener的方式

下面我们来具体看看这几个过滤器

SingleSignOutFilter

下面我们来看下该过滤器的代码:

代码语言:javascript
复制
@Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
           final FilterChain filterChain) throws IOException, ServletException {
  final HttpServletRequest request = (HttpServletRequest) servletRequest;
  final HttpServletResponse response = (HttpServletResponse) servletResponse;

  /**
   * <p>Workaround for now for the fact that Spring Security will fail since it doesn't call {@link #init(javax.servlet.FilterConfig)}.</p>
   * <p>Ultimately we need to allow deployers to actually inject their fully-initialized {@link org.jasig.cas.client.session.SingleSignOutHandler}.</p>
   */
  if (!this.handlerInitialized.getAndSet(true)) {
    HANDLER.init();
  }

  if (HANDLER.process(request, response)) {
    filterChain.doFilter(servletRequest, servletResponse);
  }
}

首先会调用handler的初始化方法,那么该handler是什么呢?就是对应的SingleSignOutHander类,init方法如下:

代码语言:javascript
复制
public synchronized void init() {
  if (this.safeParameters == null) {
    CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null.");
    CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null.");
    CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannot be null.");
    CommonUtils.assertNotNull(this.relayStateParameterName, "relayStateParameterName cannot be null.");

    if (this.artifactParameterOverPost) {
      this.safeParameters = Arrays.asList(this.logoutParameterName, this.artifactParameterName);
    } else {
      this.safeParameters = Collections.singletonList(this.logoutParameterName);
    }
  }
}

判断是否具有logoutParameterName参数指定的参数,默认参数名为logoutRequest

随后调用了process方法:

代码语言:javascript
复制
public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
  if (isTokenRequest(request)) {
    logger.trace("Received a token request");
    recordSession(request);
    return true;
  } 
  
  if (isLogoutRequest(request)) {
    logger.trace("Received a logout request");
    destroySession(request);
    return false;
  } 
  logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
  return true;
}

可以看到有两处逻辑:

  • 1、判断是否是正常的ticket请求
  • 2、判断是否是logout登出请求

下面依次来解析。

1、ticket请求

判断是否具有artifactParameterName参数指定的参数,默认参数名为ticket。如果有,就表示是一个正常的业务请求,进入该分支。可以看到有个recordSession方法,从名字就可以看出是保存session的,我们进入其中,看看代码:

代码语言:javascript
复制
private void recordSession(final HttpServletRequest request) {
  final HttpSession session = request.getSession(this.eagerlyCreateSessions);

  if (session == null) {
    logger.debug("No session currently exists (and none created).  Cannot record session information for single sign out.");
    return;
  }

  final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);
  logger.debug("Recording session for token {}", token);

  try {
    this.sessionMappingStorage.removeBySessionById(session.getId());
  } catch (final Exception e) {
    // ignore if the session is already marked as invalid. Nothing we can do!
  }
  sessionMappingStorage.addSessionById(token, session);
}

大致逻辑就是:从request中取出session和ticket,先从存储中移除该sessionId对应的session,然后以新的tocket作为key将该session存储下来。这里的sessionMappingStorage是什么,我们后面再讲。

2、logout登出请求

判断是否logout登出请求。如果是,就表示是一个退出业务系统的请求,进入该分支。可以看到有个destroySession方法,从名字就可以看出是销毁session的,我们进入其中,看看代码:

代码语言:javascript
复制
private void destroySession(final HttpServletRequest request) {
  String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
  if (CommonUtils.isBlank(logoutMessage)) {
    logger.error("Could not locate logout message of the request from {}", this.logoutParameterName);
    return;
  }
  
  if (!logoutMessage.contains("SessionIndex")) {
    logoutMessage = uncompressLogoutMessage(logoutMessage);
  }
  
  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);
    }
  }
}

首先从request中取出token,也就是ticket,然后根据ticket从存储中移除对应的session,同时将该session销毁,最后执行登出策略的回调接口。

那么这个登出请求是在什么时候会触发呢,也就是说是谁通知的业务系统呢?这个是在你退出登录的任意ST客户端,经过cas server的时候,cas server会取得cookie里面的TGT数据,找到TGT中关联的所有ST对应的地址,也就是各个业务系统,向每个地址方式一个http请求,并传递logoutRequest参数。

SessionMappingStorage

上面我们提到了,session的存储都是通过sessionMappingStorage来的,那么现在就来来看看这个是什么?我们进入代码:

代码语言:javascript
复制
public interface SessionMappingStorage {

    /**
     * Remove the HttpSession based on the mappingId.
     * 
     * @param mappingId the id the session is keyed under.
     * @return the HttpSession if it exists.
     */
    HttpSession removeSessionByMappingId(String mappingId);

    /**
     * Remove a session by its Id.
     * @param sessionId the id of the session.
     */
    void removeBySessionById(String sessionId);

    /**
     * Add a session by its mapping Id.
     * @param mappingId the id to map the session to.
     * @param session the HttpSession.
     */
    void addSessionById(String mappingId, HttpSession session);

}

可以看到,它是一个提供了三个方法的接口类,默认提供了一个hash方式的实现类:

代码语言:javascript
复制
public final class HashMapBackedSessionMappingStorage implements SessionMappingStorage {

    /**
     * Maps the ID from the CAS server to the Session.
     */
    private final Map<String, HttpSession> MANAGED_SESSIONS = new HashMap<String, HttpSession>();

    /**
     * Maps the Session ID to the key from the CAS Server.
     */
    private final Map<String, String> ID_TO_SESSION_KEY_MAPPING = new HashMap<String, String>();

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public synchronized void addSessionById(final String mappingId, final HttpSession session) {
        ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId);
        MANAGED_SESSIONS.put(mappingId, session);

    }

    @Override
    public synchronized void removeBySessionById(final String sessionId) {
        logger.debug("Attempting to remove Session=[{}]", sessionId);

        final String key = ID_TO_SESSION_KEY_MAPPING.get(sessionId);

        if (logger.isDebugEnabled()) {
            if (key != null) {
                logger.debug("Found mapping for session.  Session Removed.");
            } else {
                logger.debug("No mapping for session found.  Ignoring.");
            }
        }
        MANAGED_SESSIONS.remove(key);
        ID_TO_SESSION_KEY_MAPPING.remove(sessionId);
    }

    @Override
    public synchronized HttpSession removeSessionByMappingId(final String mappingId) {
        final HttpSession session = MANAGED_SESSIONS.get(mappingId);

        if (session != null) {
            removeBySessionById(session.getId());
        }

        return session;
    }
}

也就是通过hashmap来实现内存中session的存储,大家可能就已经想到了,如果是分布式环境下,这个实现类就不合适了,需要自己实现,具体方案后面新开一篇文章详解。

SingleSignOutHttpSessionListener

有时候,如果我们需要在系统退出之后,做一些额外的处理,则可以直接加上该监听,当然,也可以自定义一个HttpSessionListener接口类,在该类中关联该SingleSignOutHttpSessionListener类,同时增加一些额外的处理即可。下面我们来看下该监听默认的实现:

代码语言:javascript
复制
public final class SingleSignOutHttpSessionListener implements HttpSessionListener {

    private SessionMappingStorage sessionMappingStorage;

    @Override
    public void sessionCreated(final HttpSessionEvent event) {
        // nothing to do at the moment
    }

    @Override
    public void sessionDestroyed(final HttpSessionEvent event) {
        if (sessionMappingStorage == null) {
            sessionMappingStorage = getSessionMappingStorage();
        }
        final HttpSession session = event.getSession();
        sessionMappingStorage.removeBySessionById(session.getId());
    }

    /**
     * Obtains a {@link SessionMappingStorage} object. Assumes this method will always return the same
     * instance of the object.  It assumes this because it generally lazily calls the method.
     * 
     * @return the SessionMappingStorage
     */
    protected static SessionMappingStorage getSessionMappingStorage() {
        return SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage();
    }
}

很简单,也就是使用sessionMappingStorage进行session的销毁。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 有文化的技术人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 配置文件
  • SingleSignOutFilter
    • 1、ticket请求
      • 2、logout登出请求
      • SessionMappingStorage
      • SingleSignOutHttpSessionListener
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档