前言
- 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 {
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;
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 cookie = new Cookie(cookieName, null);
String contextPath = request.getContextPath();
String cookiePath = StringUtils.hasText(contextPath) ? contextPath : "/";
cookie.setPath(cookiePath);
cookie.setMaxAge(0);
cookie.setSecure(request.isSecure());
return cookie;
});
}
this.cookiesToClear = cookieList;
}
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;
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 {
private boolean invalidateHttpSession = true;
private boolean clearAuthentication = true;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
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()));
}
}
}
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) {
PermitAllSupport.permitAll(http, this.logoutSuccessUrl);
PermitAllSupport.permitAll(http, this.getLogoutRequestMatcher(http));
}
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter != null && !isCustomLogoutSuccess()) {
loginPageGeneratingFilter.setLogoutSuccessUrl(getLogoutSuccessUrl());
}
}
- 至于为什么要将登出成功Ulr放到登录页过滤器中,看下面的代码,这是在登录页过滤器中
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
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);
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);
}