乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

LogoutFilter

定义与作用

LogoutFilter 是 Spring Security 过滤器链中负责处理登出请求的核心过滤器。它位于 CsrfFilter 之后、UsernamePasswordAuthenticationFilter 之前,拦截配置的登出 URL(默认为 /logout),执行一系列清理操作:注销 Session、清除 SecurityContext、删除 RememberMe Token、调用自定义的 LogoutHandler 链,最后重定向到登出成功页面。


核心原理

1. LogoutHandler 链

LogoutFilter 内部维护一个 List<LogoutHandler>,登出时按顺序执行:

默认 Handler职责
SecurityContextLogoutHandler清除 SecurityContextHolder 中的 Authentication
ClearSiteDataLogoutHandler(可选)发送 Clear-Site-Data 响应头,清除浏览器 Cookie/Storage
CookieClearingLogoutHandler删除指定的 Cookie(如 JSESSIONID、remember-me)
SessionInvalidationLogoutHandler调用 HttpSession.invalidate() 使 Session 失效
RememberMeServices.logout()清除持久化 RememberMe Token(如数据库记录)
// LogoutFilter 核心逻辑(简化)
public void doFilter(HttpServletRequest request, 
                     HttpServletResponse response, 
                     FilterChain chain) {
    if (requiresLogout(request, response)) {
        // 1. 获取当前认证信息
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        
        // 2. 依次执行所有 LogoutHandler
        for (LogoutHandler handler : handlers) {
            handler.logout(request, response, auth);
        }
        
        // 3. 调用 LogoutSuccessHandler
        logoutSuccessHandler.onLogoutSuccess(request, response, auth);
        return;  // 不继续执行后续过滤器
    }
    chain.doFilter(request, response);
}

2. LogoutConfigurer 配置项


完整示例一:前后端分离的 JSON 登出响应

场景说明

前后端分离应用,前端通过 AJAX 调用 /api/logout 接口。需要返回 JSON 响应(而非 HTML 重定向),同时清除后端 Session 和前端相关 Cookie。

操作前配置(默认行为,返回 HTML 重定向)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .logout(Customizer.withDefaults());  // 默认 /logout,POST,成功后 302 到 /login?logout
    return http.build();
}

问题分析:默认 LogoutSuccessHandler 是 SimpleUrlLogoutSuccessHandler,执行后返回 302 重定向到 /login?logout。前端 AJAX 无法正确处理 302,可能在控制台报错或需要额外处理重定向逻辑。

操作后配置(自定义 JSON 响应)

@Configuration
@EnableWebSecurity
public class ApiLogoutConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .httpBasic(Customizer.withDefaults())
            .logout(logout -> logout
                .logoutUrl("/api/logout")  // 自定义登出 URL
                .invalidateHttpSession(true)  // 使 Session 失效
                .clearAuthentication(true)  // 清除 SecurityContext
                .deleteCookies("JSESSIONID", "remember-me")  // 删除指定 Cookie
                .logoutSuccessHandler((request, response, authentication) -> {
                    // 返回 JSON 而不是重定向
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write(
                        "{\"code\":200,\"message\":\"登出成功\"}"
                    );
                })
            );
        return http.build();
    }
}

前端配合代码

// 前端 Axios 调用登出
async function logout() {
    try {
        const response = await axios.post('/api/logout', {}, {
            withCredentials: true  // 确保携带 Cookie
        });
        console.log(response.data);  // {code: 200, message: "登出成功"}
        // 清除前端本地存储的 Token 或状态
        localStorage.removeItem('user_info');
        window.location.href = '/login';
    } catch (error) {
        console.error('登出失败', error);
    }
}

结果分析

请求服务器行为响应前端行为
POST /api/logout(已登录)Session 失效、SecurityContext 清除、Cookie 删除200 JSON {code:200, message:"登出成功"}读取 JSON,跳转登录页
POST /api/logout(未登录)无 Session 可清除,handler 仍执行200 JSON提示已登出
GET /api/logout默认仅匹配 POST,GET 请求被放行到后续过滤器可能 404 或到达 Controller前端应使用 POST 调用

完整示例二:自定义 LogoutHandler 实现审计日志

场景说明

企业应用要求记录用户登出行为(审计日志),包括登出时间、IP 地址、用户名。需要自定义 LogoutHandler 在登出时写入日志数据库。

操作前配置(无审计记录)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .logout(Customizer.withDefaults());
    return http.build();
}

问题分析:默认登出只清理安全上下文,不记录审计信息。安全团队无法追踪用户何时、从哪里登出,不利于安全审计和问题排查。

操作后配置(添加审计 Handler)

@Configuration
@EnableWebSecurity
public class AuditLogoutConfig {

    @Autowired
    private AuditLogRepository auditLogRepository;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
            .logout(logout -> logout
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .addLogoutHandler(new AuditLogoutHandler(auditLogRepository))
                .logoutSuccessUrl("/login?logout")
            );
        return http.build();
    }

    // 自定义审计 LogoutHandler
    public static class AuditLogoutHandler implements LogoutHandler {
        
        private final AuditLogRepository auditLogRepository;
        
        public AuditLogoutHandler(AuditLogRepository auditLogRepository) {
            this.auditLogRepository = auditLogRepository;
        }
        
        @Override
        public void logout(HttpServletRequest request, 
                          HttpServletResponse response, 
                          Authentication authentication) {
            if (authentication != null && authentication.getName() != null) {
                String username = authentication.getName();
                String ipAddress = request.getRemoteAddr();
                String userAgent = request.getHeader("User-Agent");
                LocalDateTime logoutTime = LocalDateTime.now();
                
                AuditLog log = new AuditLog();
                log.setUsername(username);
                log.setAction("LOGOUT");
                log.setIpAddress(ipAddress);
                log.setUserAgent(userAgent);
                log.setTimestamp(logoutTime);
                
                auditLogRepository.save(log);
                
                System.out.println("[AUDIT] 用户 " + username + " 从 IP " + ipAddress + " 登出");
            }
        }
    }
}

// 审计日志实体(简化)
@Entity
public class AuditLog {
    @Id @GeneratedValue
    private Long id;
    private String username;
    private String action;  // LOGIN / LOGOUT / ACCESS_DENIED
    private String ipAddress;
    private String userAgent;
    private LocalDateTime timestamp;
    // getters / setters
}

// 审计日志仓库
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
}

测试验证与结果分析

测试场景用户操作审计日志记录说明
正常登出user 点击登出username=user, action=LOGOUT, IP=192.168.1.10Handler 在 SecurityContext 清除前执行,authentication 非空
Session 过期后访问用户点击登出无记录(或 username=anonymous)Session 已过期,authentication 可能为 AnonymousAuthenticationToken
多标签页登出标签页A 登出,标签页B 刷新标签页A 记录一次每个 /logout 请求触发一次 Handler
清除 Cookie 后登出用户手动删除 JSESSIONID 后点击登出无记录或匿名无有效 Session 时无法识别用户身份

易错场景:登出 URL 被 CSRF 拦截或授权规则错误覆盖

问题现象

开发者配置了自定义的登出 URL /api/auth/logout,但发现:

  1. POST 请求被 CsrfFilter 拦截,返回 403。
  2. 或者登出 URL 被 .authorizeHttpRequests() 的 anyRequest().authenticated() 覆盖,但奇怪的是——如果用户已登录,这不应该有问题。真正的问题是:将登出 URL 配置为 permitAll() 但 CSRF 未正确配置,或者 登出 URL 路径与 securityMatcher 不匹配。

错误配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/register").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults())
        .logout(logout -> logout
            .logoutUrl("/api/auth/logout")  // 自定义路径
        );
    return http.build();
}

问题分析:在 Spring Security 5.x 中,/logout 默认被框架隐式允许(即使未在 authorizeHttpRequests 中显式配置)。但如果改为自定义路径 /api/auth/logout,它不会被隐式放行。如果用户已登录,访问 /api/auth/logout 可以正常通过授权检查。但如果开发者同时使用了 csrf.disable() 或需要前端发送 POST 请求时,问题会出现。

真正典型的陷阱是以下这种多链配置中的登出问题:

@Bean
@Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults())
        .csrf(csrf -> csrf.disable())  // API 链禁用 CSRF
        .logout(logout -> logout
            .logoutUrl("/api/logout")  // 在 /api/** 链中配置
        );
    return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/web/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .csrf(Customizer.withDefaults())  // Web 链启用 CSRF
        .logout(logout -> logout
            .logoutUrl("/web/logout")  // 在 /web/** 链中配置
        );
    return http.build();
}

陷阱:如果用户通过 /web/** 的表单登录链认证,然后访问 /api/logout,请求会被 apiSecurity 链匹配(因为 /api/logout 以 /api/ 开头)。此时 apiSecurity 链中的 LogoutFilter 处理登出,但 apiSecurity 链使用 httpBasic 认证,且用户此前通过 /web/** 的表单登录建立了 Session 认证。虽然 LogoutFilter 能执行 invalidateHttpSession() 使 Session 失效,但 apiSecurity 链的 httpBasic 配置和 csrf.disable() 意味着这个链的设计是无状态的。如果用户期望通过表单登录链的 Session 登出,而实际访问了 API 链的 logout URL,可能会因为 httpBasic 的认证机制差异导致行为不一致。

正确做法

  1. 登出 URL 应与登录链属于同一条 SecurityFilterChain,确保认证机制一致。
  2. 如果必须使用自定义登出 URL,确保它在授权规则中正确放行(或至少对已认证用户可访问)。
  3. 如果启用 CSRF,登出请求必须是 POST,并携带 CSRF Token。
@Bean
public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/web/**", "/logout")  // 包含登出 URL
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/logout").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults())
        .csrf(Customizer.withDefaults())  // CSRF 开启
        .logout(logout -> logout
            .logoutUrl("/logout")  // 默认 POST,携带 CSRF Token
            .logoutSuccessUrl("/login?logout")
        );
    return http.build();
}
<!-- Thymeleaf 表单登出(必须 POST + CSRF Token) -->
<form th:action="@{/logout}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    <button type="submit">登出</button>
</form>

面试考点:LogoutFilter 中的 LogoutHandler 和 LogoutSuccessHandler 有什么区别?登出时 Session 是在哪个 Handler 中失效的? 答:LogoutHandler 是一组在登出时执行清理操作的处理器(责任链模式),包括 SecurityContextLogoutHandler(清除 SecurityContextHolder)、SessionInvalidationLogoutHandler(使 Session 失效)、CookieClearingLogoutHandler(删除 Cookie)等。LogoutSuccessHandler 是在所有 LogoutHandler 执行完毕后,决定登出成功后的响应行为(如重定向到登录页或返回 JSON)。Session 失效由 SessionInvalidationLogoutHandler(或 SecurityContextLogoutHandler 根据配置)在 LogoutHandler 链中执行,早于 LogoutSuccessHandler 的调用。

上一页
SecurityContextHolderFilter
下一页
BasicAuthenticationFilter