前言
- • 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);
}