前言

  • • SpringSecurity默认提供了登录的页面以及登录的接口,与之对应的也提供了登出页和登出请求
  • • 登出请求对应的过滤器是LogoutFilter
  • • 登出页对应的是DefaultLogoutPageGeneratingFilter、

1. LogoutConfigurer

  • • LogoutConfigurer是LogoutFilter对应的配置类,先看其主要方法

1.1 addLogoutHandler(...)

  • • 为LogoutFilter添加对应的登出处理器(LogoutHandler)
public LogoutConfigurer<H> addLogoutHandler(LogoutHandler logoutHandler) {
   Assert.notNull(logoutHandler, "logoutHandler cannot be null");
   this.logoutHandlers.add(logoutHandler);
   return this;
}
  • • LogoutHandler作为登出时的主要操作对象
public interface LogoutHandler {

   /**
    * 进行登出操作,比如说清除Csrf的令牌
    * @param request the HTTP request
    * @param response the HTTP response
    * @param authentication the current principal details
    */
   void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication);

}
  • • 其子类有很多

1.1.1 CookieClearingLogoutHandler

  • • 如果说我们有一些比较重要的Cookie需要在退出登录后清除,那就可以用到CookieClearingLogoutHandler,有两种清空处理方式
  • • 将指定Cookie的值设置为空
  • • 将指定Cookie的生存时间设置为0
public final class CookieClearingLogoutHandler implements LogoutHandler {

   private final List<Function<HttpServletRequest, Cookie>> cookiesToClear;

   /**
    * 将指定Cookie的值设置为空
    * @param cookiesToClear 需要清除Cookie的名称
    */
   public CookieClearingLogoutHandler(String... cookiesToClear) {
      Assert.notNull(cookiesToClear, "List of cookies cannot be null");
      List<Function<HttpServletRequest, Cookie>> cookieList = new ArrayList<>();
      for (String cookieName : cookiesToClear) {
         //添加清除函数
         cookieList.add((request) -> {
            //这里将指定名称的Cookie的Value设置为空
            Cookie cookie = new Cookie(cookieName, null);
            String contextPath = request.getContextPath();
            String cookiePath = StringUtils.hasText(contextPath) ? contextPath : "/";
            cookie.setPath(cookiePath);
            cookie.setMaxAge(0);
            //表明只能使用Https或者SSL
            cookie.setSecure(request.isSecure());
            return cookie;
         });
      }
      this.cookiesToClear = cookieList;
   }

   /**
    * 将指定Cookie的生存时间设置为0
    * @param cookiesToClear
    */
   public CookieClearingLogoutHandler(Cookie... cookiesToClear) {
      Assert.notNull(cookiesToClear, "List of cookies cannot be null");
      List<Function<HttpServletRequest, Cookie>> cookieList = new ArrayList<>();
      for (Cookie cookie : cookiesToClear) {
         Assert.isTrue(cookie.getMaxAge() == 0, "Cookie maxAge must be 0");
         cookieList.add((request) -> cookie);
      }
      this.cookiesToClear = cookieList;
   }

   @Override
   public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
      this.cookiesToClear.forEach((f) -> response.addCookie(f.apply(request)));
   }

}

1.1.2 CsrfLogoutHandler

  • • 上篇文章已经介绍过了

1.1.3 HeaderWriterLogoutHandler

  • • HeaderWriterLogoutHandler:将指定请求头写入响应头的登出处理器
public final class HeaderWriterLogoutHandler implements LogoutHandler {

   private final HeaderWriter headerWriter;

   /**
    * Constructs a new instance using the passed {@link HeaderWriter} implementation
    * @param headerWriter
    * @throws IllegalArgumentException if headerWriter is null.
    */
   public HeaderWriterLogoutHandler(HeaderWriter headerWriter) {
      Assert.notNull(headerWriter, "headerWriter cannot be null");
      this.headerWriter = headerWriter;
   }

   @Override
   public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
      this.headerWriter.writeHeaders(request, response);
   }

}

1.1.4 LogoutSuccessEventPublishingLogoutHandler

  • • LogoutSuccessEventPublishingLogoutHandler:发布登出事件的登出处理器
  • • 也是默认注册的LogoutHandler之一
public final class LogoutSuccessEventPublishingLogoutHandler implements LogoutHandler, ApplicationEventPublisherAware {

   private ApplicationEventPublisher eventPublisher;

   @Override
   public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
      if (this.eventPublisher == null) {
         return;
      }
      if (authentication == null) {
         return;
      }
      this.eventPublisher.publishEvent(new LogoutSuccessEvent(authentication));
   }

   @Override
   public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
      this.eventPublisher = applicationEventPublisher;
   }

}

1.1.5 PersistentTokenBasedRememberMeServices && TokenBasedRememberMeServices

  • • SpringSecurity支持记住我机制,本质上也是提供一个记住我令牌到Cookie中,所以说需要在登出的时候删除存储在服务器的记住我令牌,这两个也正是干这个的
  • • 由于此类并不仅仅是干这个的,所以说只贴出有关于登出的代码
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
   super.logout(request, response, authentication);
   if (authentication != null) {
      this.tokenRepository.removeUserTokens(authentication.getName());
   }
}

1.1.6 SecurityContextLogoutHandler

  • • SecurityContextLogoutHandler:SecurityContext作为SpringSecurity的核心类,保存了认证信息和用户信息,是至关重要的,所以说需要在登出的时候有一个类负责清空SecurityContext
  • • 同时这也是默认的登出处理器之一
public class SecurityContextLogoutHandler implements LogoutHandler {

   /**
    * 是否应该在登出时 使Session无效
    */
   private boolean invalidateHttpSession = true;

   /**
    * 是否应该在登出时 清除认证对象
    */
   private boolean clearAuthentication = true;

   /**
    * 清空当前用户的安全上下文
    * @param request the HTTP request
    * @param response the HTTP response
    * @param authentication the current principal details
    */
   @Override
   public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
      Assert.notNull(request, "HttpServletRequest required");
      //是否使Session无效
      if (this.invalidateHttpSession) {
         HttpSession session = request.getSession(false);
         if (session != null) {
            session.invalidate();
            if (this.logger.isDebugEnabled()) {
               this.logger.debug(LogMessage.format("Invalidated session %s", session.getId()));
            }
         }
      }

      //清空当前用户的 线程级别的安全上下文
      //HttpSession级别的在SecurityContextPersistenceFilter中会被清除
      SecurityContext context = SecurityContextHolder.getContext();
      SecurityContextHolder.clearContext();
      //清空认证对象
      if (this.clearAuthentication) {
         context.setAuthentication(null);
      }
   }
}

1.2 logoutSuccessHandler(...)

  • • logoutSuccessHandler(...):配置登出成功后该干嘛,比如说跳转到哪个页面
public LogoutConfigurer<H> logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) {
   this.logoutSuccessUrl = null;
   this.customLogoutSuccess = true;
   this.logoutSuccessHandler = logoutSuccessHandler;
   return this;
}
  • • 其两个实现类一个是转发,一个是设置响应码,都很简单就不做介绍了
  • • ForwardLogoutSuccessHandler
  • • HttpStatusReturningLogoutSuccessHandler

1.3 init(...)

  • • 讲这个方法之前,先来回顾下SpringSsecurity的构建流程
  • • 可以看出是先执行init()方法才会执行configure()方法;
@Override
protected final O doBuild() throws Exception {
   synchronized (this.configurers) {
      this.buildState = BuildState.INITIALIZING;
      beforeInit();
      init();
      this.buildState = BuildState.CONFIGURING;
      beforeConfigure();
      configure();
      this.buildState = BuildState.BUILDING;
      O result = performBuild();
      this.buildState = BuildState.BUILT;
      return result;
   }
}
  • • 我们再来看init(...)的源码
  • • 代码很少就是放行登出请求的Url以及将登出成功Ulr放到登录页过滤器中
@Override
public void init(H http) {
   //如果允许放行
   if (this.permitAll) {
      //两个都是放行登出成功Url
      PermitAllSupport.permitAll(http, this.logoutSuccessUrl);
      PermitAllSupport.permitAll(http, this.getLogoutRequestMatcher(http));
   }

   DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
         .getSharedObject(DefaultLoginPageGeneratingFilter.class);
   //当有登录页配置类的时候并且用户没有自定义了登出成功跳转的Url/处理器
   if (loginPageGeneratingFilter != null && !isCustomLogoutSuccess()) {
      //设置登录页的登出成功Url
      loginPageGeneratingFilter.setLogoutSuccessUrl(getLogoutSuccessUrl());
   }
}
  • • 至于为什么要将登出成功Ulr放到登录页过滤器中,看下面的代码,这是在登录页过滤器中
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
          throws IOException, ServletException {
       //是否是认证失败Url的请求
       boolean loginError = isErrorPage(request);
       //是否是登出成功的请求
       boolean logoutSuccess = isLogoutSuccess(request);
       //判断是否需要生产登录页
       if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
          String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
          response.setContentType("text/html;charset=UTF-8");
          response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
          response.getWriter().write(loginPageHtml);
          return;
       }
       chain.doFilter(request, response);
    }
}
  • • 如果说登出的url是靠ForwardLogoutSuccessHandler进行转发的,那么就又会进入过滤器链,这个时候DefaultLoginPageGeneratingFilter会干嘛?
  • • 很明显会直接生成登录页的Html代码返回给浏览器

1.4 configure(...)

  • • configure(...)的代码很少,主要集中在createLogoutFilter(http)方法中
@Override
public void configure(H http) throws Exception {
   LogoutFilter logoutFilter = createLogoutFilter(http);
   http.addFilter(logoutFilter);
}
  • • createLogoutFilter(...)方法的代码无非就是将我前面讲的类封装到LogoutFilter中
private LogoutFilter createLogoutFilter(H http) {
   //添加登出处理器
   this.logoutHandlers.add(this.contextLogoutHandler);
   //这里多执行了postProcess()方法,是因为这个登出处理器需要一个ApplicationEventPublisher
   this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));

   //所有的登出处理器
   LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
   //创建过滤器
   LogoutFilter result = new LogoutFilter(
         //获得登出成功处理器
         getLogoutSuccessHandler()
         , handlers);
   //设置登出请求的匹配器
   result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
   result = postProcess(result);
   return result;
}

2. LogoutFilter

  • • 直接上核心方法,原理很简单
  • • 先执行登出处理器
  • • 再执行登出成功处理器
  • • 判断是否是登出请求,如果是
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws IOException, ServletException {
   //判断是否是登出请求
   if (requiresLogout(request, response)) {
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      if (this.logger.isDebugEnabled()) {
         this.logger.debug(LogMessage.format("Logging out [%s]", auth));
      }
      //先执行登出处理器
      this.handler.logout(request, response, auth);
      //再执行登出成功处理器
      this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
      return;
   }
   chain.doFilter(request, response);
}