作为一个配置HttpSecurity
的SecurityConfigurer
,LogoutConfigurer
的配置任务如下 :
配置如下安全过滤器Filter
LogoutFilter
logoutUrl
,logoutRequestMatcher
,以及配置器CsrfConfigurer
中是否启用csrf
保护等信息构建最终所使用的退出LogoutRequestMatcher
,也就是触发退出的url
匹配器。logoutSuccessHandler
,defaultLogoutSuccessHandlerMappings
,logoutSuccessUrl
确定最终要使用LogoutSuccessHandler
,它们三个之间的应用优先级是 : logoutSuccessHandler > defaultLogoutSuccessHandlerMappings > logoutSuccessUrl
。LogoutConfigurer
所被设置的所有LogoutHandler
都会被应用,最后一个LogoutHandler
总是LogoutConfigurer
自己提供的一个SecurityContextLogoutHandler
,该SecurityContextLogoutHandler
也可以被调用者设置。创建如下共享对象
// HttpSecurity 代码片段
public LogoutConfigurer<HttpSecurity> logout() throws Exception {
return getOrApply(new LogoutConfigurer<>());
}
源代码版本 Spring Security Config 5.1.4.RELEASE
package org.springframework.security.config.annotation.web.configurers;
// 省略 imports
public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractHttpConfigurer<LogoutConfigurer<H>, H> {
// 退出时要执行的处理器,可以有多个, contextLogoutHandler 总是最后一个
private List<LogoutHandler> logoutHandlers = new ArrayList<>();
private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
// 退出成功时跳转的页面,缺省使用 /login?logout
private String logoutSuccessUrl = "/login?logout";
// 退出成功时的处理器,如果该属性被设置,logoutSuccessUrl 的值会被忽略
private LogoutSuccessHandler logoutSuccessHandler;
// 触发退出登录的url,缺省使用 /logout
// 如果是使用了 csrf 保护,访问该地址必须使用 POST, 如果没有使用 csrf 保护, 也可以使用 GET,
// PUT,DELETE
// logoutUrl 和 logoutRequestMatcher 通常二选其一使用,最佳实践建议使用 logoutUrl
private String logoutUrl = "/logout";
// 触发退出登录的 RequestMatcher, 缺省为 null
// logoutUrl 和 logoutRequestMatcher 通常二选其一使用,最佳实践建议使用 logoutUrl
private RequestMatcher logoutRequestMatcher;
private boolean permitAll;
// 如果 logoutSuccessHandler 被设置,或者 logoutSuccessUrl 被设置为非缺省值,
// customLogoutSuccess 为 true
private boolean customLogoutSuccess;
private LinkedHashMap<RequestMatcher, LogoutSuccessHandler> defaultLogoutSuccessHandlerMappings
= new LinkedHashMap<>();
/**
* Creates a new instance
* @see HttpSecurity#logout()
*/
public LogoutConfigurer() {
}
/**
* Adds a LogoutHandler. The SecurityContextLogoutHandler is added as
* the last LogoutHandler by default.
*
* @param logoutHandler the LogoutHandler to add
* @return the LogoutConfigurer for further customization
*/
public LogoutConfigurer<H> addLogoutHandler(LogoutHandler logoutHandler) {
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
this.logoutHandlers.add(logoutHandler);
return this;
}
/**
* Specifies if SecurityContextLogoutHandler should clear the Authentication at
* the time of logout.
* @param clearAuthentication true SecurityContextLogoutHandler should clear the
* Authentication (default), or false otherwise.
* @return the LogoutConfigurer for further customization
*/
public LogoutConfigurer<H> clearAuthentication(boolean clearAuthentication) {
contextLogoutHandler.setClearAuthentication(clearAuthentication);
return this;
}
/**
* Configures SecurityContextLogoutHandler to invalidate the
* HttpSession at the time of logout.
* @param invalidateHttpSession true if the HttpSession should be invalidated
* (default), or false otherwise.
* @return the LogoutConfigurer for further customization
*/
public LogoutConfigurer<H> invalidateHttpSession(boolean invalidateHttpSession) {
contextLogoutHandler.setInvalidateHttpSession(invalidateHttpSession);
return this;
}
/**
* The URL that triggers log out to occur (default is "/logout"). If CSRF protection
* is enabled (default), then the request must also be a POST. This means that by
* default POST "/logout" is required to trigger a log out. If CSRF protection is
* disabled, then any HTTP method is allowed.
*
*
* It is considered best practice to use an HTTP POST on any action that changes state
* (i.e. log out) to protect against CSRF attacks. If
* you really want to use an HTTP GET, you can use
* logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET"));
*
*
* @see #logoutRequestMatcher(RequestMatcher)
* @see HttpSecurity#csrf()
*
* @param logoutUrl the URL that will invoke logout.
* @return the LogoutConfigurer for further customization
*/
public LogoutConfigurer<H> logoutUrl(String logoutUrl) {
// 使用 logoutUrl 的时候将 logoutRequestMatcher 设置为 null
this.logoutRequestMatcher = null;
this.logoutUrl = logoutUrl;
return this;
}
/**
* The RequestMatcher that triggers log out to occur. In most circumstances users will
* use #logoutUrl(String) which helps enforce good practices.
*
* @see #logoutUrl(String)
*
* @param logoutRequestMatcher the RequestMatcher used to determine if logout should
* occur.
* @return the LogoutConfigurer for further customization
*/
public LogoutConfigurer<H> logoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
this.logoutRequestMatcher = logoutRequestMatcher;
return this;
}
/**
* The URL to redirect to after logout has occurred. The default is "/login?logout".
* This is a shortcut for invoking #logoutSuccessHandler(LogoutSuccessHandler)
* with a SimpleUrlLogoutSuccessHandler.
*
* @param logoutSuccessUrl the URL to redirect to after logout occurred
* @return the LogoutConfigurer for further customization
*/
public LogoutConfigurer<H> logoutSuccessUrl(String logoutSuccessUrl) {
this.customLogoutSuccess = true;
this.logoutSuccessUrl = logoutSuccessUrl;
return this;
}
/**
* A shortcut for #permitAll(boolean) with true as an argument.
* @return the LogoutConfigurer for further customizations
*/
public LogoutConfigurer<H> permitAll() {
return permitAll(true);
}
/**
* Allows specifying the names of cookies to be removed on logout success. This is a
* shortcut to easily invoke #addLogoutHandler(LogoutHandler) with a
* CookieClearingLogoutHandler.
*
* @param cookieNamesToClear the names of cookies to be removed on logout success.
* @return the LogoutConfigurer for further customization
*/
public LogoutConfigurer<H> deleteCookies(String... cookieNamesToClear) {
// 设置退出成功时要删除的cookie的名称
// 其实是添加了一个退出处理器 CookieClearingLogoutHandler
return addLogoutHandler(new CookieClearingLogoutHandler(cookieNamesToClear));
}
/**
* Sets the LogoutSuccessHandler to use. If this is specified,
* #logoutSuccessUrl(String) is ignored.
*
* @param logoutSuccessHandler the LogoutSuccessHandler to use after a user
* has been logged out.
* @return the LogoutConfigurer for further customizations
*/
public LogoutConfigurer<H> logoutSuccessHandler(
LogoutSuccessHandler logoutSuccessHandler) {
this.logoutSuccessUrl = null; // 设置 logoutSuccessUrl 为 null
this.customLogoutSuccess = true;
this.logoutSuccessHandler = logoutSuccessHandler;
return this;
}
/**
* Sets a default LogoutSuccessHandler to be used which prefers being invoked
* for the provided RequestMatcher. If no LogoutSuccessHandler is
* specified a SimpleUrlLogoutSuccessHandler will be used.
* If any default LogoutSuccessHandler instances are configured, then a
* DelegatingLogoutSuccessHandler will be used that defaults to a
* SimpleUrlLogoutSuccessHandler.
*
* @param handler the LogoutSuccessHandler to use
* @param preferredMatcher the RequestMatcher for this default
* LogoutSuccessHandler
* @return the LogoutConfigurer for further customizations
*/
public LogoutConfigurer<H> defaultLogoutSuccessHandlerFor(
LogoutSuccessHandler handler, RequestMatcher preferredMatcher) {
Assert.notNull(handler, "handler cannot be null");
Assert.notNull(preferredMatcher, "preferredMatcher cannot be null");
this.defaultLogoutSuccessHandlerMappings.put(preferredMatcher, handler);
return this;
}
/**
* Grants access to the #logoutSuccessUrl(String) and the
* #logoutUrl(String) for every user.
*
* 对 logoutSuccessUrl, logoutUrl 的用户访问权限进行设置
* @param permitAll if true grants access, else nothing is done
* @return the LogoutConfigurer for further customization.
*/
public LogoutConfigurer<H> permitAll(boolean permitAll) {
this.permitAll = permitAll;
return this;
}
/**
* Gets the LogoutSuccessHandler if not null, otherwise creates a new
* SimpleUrlLogoutSuccessHandler using the #logoutSuccessUrl(String).
* 如果设置了 logoutSuccessHandler,则使用 logoutSuccessHandler, 否则
* 基于 logoutSuccessUrl 和 defaultLogoutSuccessHandlerMappings 创建缺省的 LogoutSuccessHandler
* 并最终使用
* @return the LogoutSuccessHandler to use
*/
private LogoutSuccessHandler getLogoutSuccessHandler() {
LogoutSuccessHandler handler = this.logoutSuccessHandler;
if (handler == null) {
handler = createDefaultSuccessHandler();
}
return handler;
}
// 基于 logoutSuccessUrl 和 defaultLogoutSuccessHandlerMappings 创建缺省的 LogoutSuccessHandler
// 1. 如果 defaultLogoutSuccessHandlerMappings 不为空,则使用基于它的 DelegatingLogoutSuccessHandler
// 2. 如果 defaultLogoutSuccessHandlerMappings 为空,使用基于 logoutSuccessUrl 创建的
// SimpleUrlLogoutSuccessHandler
private LogoutSuccessHandler createDefaultSuccessHandler() {
SimpleUrlLogoutSuccessHandler urlLogoutHandler = new SimpleUrlLogoutSuccessHandler();
urlLogoutHandler.setDefaultTargetUrl(logoutSuccessUrl);
if (defaultLogoutSuccessHandlerMappings.isEmpty()) {
return urlLogoutHandler;
}
DelegatingLogoutSuccessHandler successHandler =
new DelegatingLogoutSuccessHandler(defaultLogoutSuccessHandlerMappings);
successHandler.setDefaultLogoutSuccessHandler(urlLogoutHandler);
return successHandler;
}
@Override
public void init(H http) throws Exception {
// 根据 permitAll 属性,对 HttpSecurity 构建器 http 进行设置如下 url 放行设置 :
// 1. this.logoutSuccessUrl : 退出成功时所跳转的 url
// 2. this.getLogoutRequestMatcher(http) 所匹配的 url : 触发退出的url
if (permitAll) {
PermitAllSupport.permitAll(http, this.logoutSuccessUrl);
PermitAllSupport.permitAll(http, this.getLogoutRequestMatcher(http));
}
// 对 DefaultLoginPageGeneratingFilter 进行补充设置
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter != null && !isCustomLogoutSuccess()) {
// 如果 DefaultLoginPageGeneratingFilter 存在于共享对象,并且
// 退出成功url没有使用缺省值的情况下,对 DefaultLoginPageGeneratingFilter
// 进行设置属性 logoutSuccessUrl
loginPageGeneratingFilter.setLogoutSuccessUrl(getLogoutSuccessUrl());
}
}
@Override
public void configure(H http) throws Exception {
// 创建 LogoutFilter 过滤器添加到 HttpSecurity http
LogoutFilter logoutFilter = createLogoutFilter(http);
http.addFilter(logoutFilter);
}
/**
* Returns true if the logout success has been customized via
* #logoutSuccessUrl(String) or
* #logoutSuccessHandler(LogoutSuccessHandler).
*
* @return true if logout success handling has been customized, else false
*/
boolean isCustomLogoutSuccess() {
return customLogoutSuccess;
}
/**
* Gets the logoutSuccesUrl or null if a
* #logoutSuccessHandler(LogoutSuccessHandler) was configured.
*
* @return the logoutSuccessUrl
*/
private String getLogoutSuccessUrl() {
return logoutSuccessUrl;
}
/**
* Gets the LogoutHandler instances that will be used.
* @return the LogoutHandler instances. Cannot be null.
*/
List<LogoutHandler> getLogoutHandlers() {
return logoutHandlers;
}
/**
* Creates the LogoutFilter using the LogoutHandler instances, the
* #logoutSuccessHandler(LogoutSuccessHandler) and the
* #logoutUrl(String).
*
* @param http the builder to use
* @return the LogoutFilter to use.
* @throws Exception
*/
private LogoutFilter createLogoutFilter(H http) throws Exception {
// 这里确保 contextLogoutHandler 总是所有 logoutHandlers 中的最后一个
logoutHandlers.add(contextLogoutHandler);
LogoutHandler[] handlers = logoutHandlers
.toArray(new LogoutHandler[logoutHandlers.size()]);
// 构造 LogoutFilter 并返回
LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);
// 根据属性 logoutRequestMatcher, url 设置, csrf 保护设置构建最终使用的
// logoutRequestMatcher 并设置到 LogoutFilter 上
result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
result = postProcess(result);
return result;
}
// 根据设置构建最终使用的 logoutRequestMatcher:
// 1. 如果设置了属性 logoutRequestMatcher , 则使用属性 logoutRequestMatcher,否则
// 2. 如果启用了 csrf 保护,使用 logoutUrl, POST 构建一个 AntPathRequestMatcher
// 到 logoutRequestMatcher 属性并最终使用,
// 3. 如果没有启用 csrf 保护,使用 logoutUrl,GET/POST/PUT/DELETE 构造一个
// OrRequestMatcher 到 logoutRequestMatcher 属性并最终使用,
@SuppressWarnings("unchecked")
private RequestMatcher getLogoutRequestMatcher(H http) {
if (logoutRequestMatcher != null) {
return logoutRequestMatcher;
}
if (http.getConfigurer(CsrfConfigurer.class) != null) {
this.logoutRequestMatcher = new AntPathRequestMatcher(this.logoutUrl, "POST");
}
else {
this.logoutRequestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(this.logoutUrl, "GET"),
new AntPathRequestMatcher(this.logoutUrl, "POST"),
new AntPathRequestMatcher(this.logoutUrl, "PUT"),
new AntPathRequestMatcher(this.logoutUrl, "DELETE")
);
}
return this.logoutRequestMatcher;
}
}