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

    • 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
    • 最佳实践

RequestCacheAwareFilter

定义与作用

RequestCacheAwareFilter 是 Spring Security 过滤器链中负责恢复被中断请求的过滤器。它位于 BasicAuthenticationFilter 之后、SecurityContextHolderAwareRequestFilter 之前。当未认证用户访问受保护资源时,ExceptionTranslationFilter 会先将该请求保存到 RequestCache(默认 HttpSessionRequestCache),然后引导用户登录。用户认证成功后,RequestCacheAwareFilter 从 RequestCache 中取出被保存的请求,恢复原始请求路径和参数,实现"登录后回到之前访问的页面"的体验。


核心原理

1. RequestCache 接口

public interface RequestCache {
    // 保存当前请求(通常在 ExceptionTranslationFilter 中调用)
    void saveRequest(HttpServletRequest request, HttpServletResponse response);
    
    // 获取被保存的请求(RequestCacheAwareFilter 中调用)
    SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
    
    // 移除被保存的请求(登录成功后清理)
    void removeRequest(HttpServletRequest request, HttpServletResponse response);
}

默认实现 HttpSessionRequestCache 将 SavedRequest 以 SPRING_SECURITY_SAVED_REQUEST 为属性名存入 HttpSession。

2. 请求保存与恢复的完整生命周期

注意:实际流程中,UsernamePasswordAuthenticationFilter 认证成功后的重定向由 SavedRequestAwareAuthenticationSuccessHandler 处理,它会从 RequestCache 获取并移除 SavedRequest。RequestCacheAwareFilter 主要处理的是其他场景下(如 RememberMe 登录、OAuth2 回调等)被保存请求的恢复。

3. SavedRequest 包含的信息

SavedRequest 接口保存了被中断请求的完整上下文:

public interface SavedRequest {
    String getRequestUrl();           // 原始请求 URL(含查询参数)
    List<Cookie> getCookies();        // Cookie
    String getMethod();             // HTTP 方法
    List<String> getHeaderValues(String name);  // 请求头
    Collection<String> getParameterNames();     // 参数名
    List<String> getParameterValues(String name); // 参数值
}

完整示例一:自定义 RequestCache 排除特定路径

场景说明

应用中有一些资源路径不需要保存请求(如 /favicon.ico、/static/**、AJAX 请求)。如果用户访问这些路径时触发登录,恢复后应直接跳到首页而不是这些辅助资源。

操作前配置(默认缓存所有请求)

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

问题分析:默认 HttpSessionRequestCache 会保存所有被拦截的请求。当用户匿名访问 /favicon.ico 时,如果触发了认证,登录成功后会被重定向到 /favicon.ico,这极不合理。AJAX 请求被保存后,登录成功返回 HTML 登录页,前端也无法正确处理。

操作后配置(排除静态资源和 AJAX 请求)

@Configuration
@EnableWebSecurity
public class CustomRequestCacheConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/static/**", "/favicon.ico").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .exceptionHandling(exceptions -> exceptions
                .requestCache(requestCache -> requestCache
                    .requestCache(customRequestCache())
                )
            );
        return http.build();
    }

    @Bean
    public RequestCache customRequestCache() {
        HttpSessionRequestCache cache = new HttpSessionRequestCache();
        
        // 自定义 RequestMatcher:不保存以下请求
        cache.setRequestMatcher(new AndRequestMatcher(
            // 1. 不保存静态资源请求
            new NegatedRequestMatcher(
                new OrRequestMatcher(
                    new AntPathRequestMatcher("/static/**"),
                    new AntPathRequestMatcher("/favicon.ico"),
                    new AntPathRequestMatcher("/**/*.js"),
                    new AntPathRequestMatcher("/**/*.css")
                )
            ),
            // 2. 不保存 AJAX 请求(通过 X-Requested-With: XMLHttpRequest 识别)
            new NegatedRequestMatcher(
                new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")
            ),
            // 3. 不保存特定路径(如登录页本身、错误页)
            new NegatedRequestMatcher(
                new OrRequestMatcher(
                    new AntPathRequestMatcher("/login"),
                    new AntPathRequestMatcher("/error"),
                    new AntPathRequestMatcher("/logout")
                )
            )
        ));
        
        return cache;
    }
}

测试验证与结果分析

用户操作匿名访问路径是否保存到 RequestCache登录后跳转目标
访问管理后台/admin/dashboard是/admin/dashboard
访问页面时浏览器请求图标/favicon.ico否默认首页(/)
AJAX 请求数据/api/data(带 XMLHttpRequest Header)否默认首页(避免返回 HTML 到 AJAX)
直接访问登录页/login否默认首页
访问静态 CSS/static/style.css否默认首页

完整示例二:禁用 RequestCache(前后端分离 API)

场景说明

纯 REST API 应用使用 JWT 认证,无表单登录和页面跳转。RequestCache 机制完全不需要,禁用可以减少 Session 操作和无意义的缓存逻辑。

操作前配置(默认启用 RequestCache)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
        .exceptionHandling(Customizer.withDefaults());
    return http.build();
}

问题分析:默认 ExceptionTranslationFilter 使用 HttpSessionRequestCache,JWT 认证失败时(如 Token 过期),RequestCache 会尝试将当前 API 请求保存到 Session,然后调用 AuthenticationEntryPoint。对于纯 API 应用,这种保存操作毫无意义,且增加了 Session 依赖。

操作后配置(使用 NullRequestCache)

@Configuration
@EnableWebSecurity
public class StatelessApiConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .exceptionHandling(exceptions -> exceptions
                .requestCache(requestCache -> requestCache
                    .requestCache(new NullRequestCache())  // 完全禁用请求缓存
                )
                // 自定义 EntryPoint 返回 JSON 401
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write(
                        "{\"code\":401,\"message\":\"Token 无效或已过期\"}"
                    );
                })
            )
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .csrf(csrf -> csrf.disable());
        return http.build();
    }
}

NullRequestCache 的 saveRequest()、getRequest()、removeRequest() 均为空操作,不会产生任何 Session 写入。

结果分析

请求原默认行为新行为差异
JWT 过期访问 /api/data保存请求到 Session → 返回 401直接返回 401 JSON无 Session 写入,无缓存开销
未认证访问 /api/admin保存请求到 Session → 返回 401直接返回 401 JSON同样无 Session 操作
多次访问不同 API每次可能覆盖 Session 中的 SavedRequest无 Session 操作减少内存和 I/O 开销

易错场景:登录成功后重定向到错误路径

问题现象

开发者在 UsernamePasswordAuthenticationFilter 中自定义了 AuthenticationSuccessHandler,直接设置固定重定向路径:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(form -> form
            .loginPage("/login")
            .successHandler((request, response, authentication) -> {
                // 错误:直接固定重定向到首页,忽略了 RequestCache
                response.sendRedirect("/home");
            })
        );
    return http.build();
}

问题分析:自定义 AuthenticationSuccessHandler 直接 sendRedirect("/home"),完全跳过了 RequestCache 的恢复机制。即使用户之前访问 /admin/dashboard?page=2,登录后也会被强制重定向到 /home,破坏了用户体验。

正确做法

如果需要自定义登录成功后的行为,应继承或委托 SavedRequestAwareAuthenticationSuccessHandler:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(form -> form
            .loginPage("/login")
            .successHandler(new CustomSavedRequestAwareSuccessHandler())
        );
    return http.build();
}

public class CustomSavedRequestAwareSuccessHandler 
        extends SavedRequestAwareAuthenticationSuccessHandler {
    
    public CustomSavedRequestAwareSuccessHandler() {
        // 1. 设置默认目标(RequestCache 为空时跳转)
        setDefaultTargetUrl("/home");
        // 2. 始终使用默认目标 URL(可配置)
        setAlwaysUseDefaultTargetUrl(false);  // false = 优先恢复 SavedRequest
    }
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                        HttpServletResponse response,
                                        Authentication authentication) 
                                        throws IOException, ServletException {
        // 自定义逻辑:如记录登录日志、发送通知等
        logLoginSuccess(authentication);
        
        // 委托给父类,父类会处理 RequestCache 恢复
        super.onAuthenticationSuccess(request, response, authentication);
    }
    
    private void logLoginSuccess(Authentication authentication) {
        System.out.println("用户 " + authentication.getName() + " 登录成功");
    }
}

关键点:SavedRequestAwareAuthenticationSuccessHandler 的 onAuthenticationSuccess() 内部逻辑:

  1. 从 RequestCache 获取 SavedRequest。
  2. 如果存在,构造重定向 URL(原始请求路径)。
  3. 调用 RequestCache.removeRequest() 清理缓存。
  4. 如果不存在 SavedRequest,使用 defaultTargetUrl。

面试考点:RequestCacheAwareFilter 与 SavedRequestAwareAuthenticationSuccessHandler 的关系是什么?为什么有时候登录后不能正确回到原页面? 答:RequestCacheAwareFilter 在登录成功后的第一次请求中检查 RequestCache,如果存在 SavedRequest 则恢复原始请求。而 SavedRequestAwareAuthenticationSuccessHandler 在登录提交成功时直接从 RequestCache 获取被保存的请求并进行重定向。两者都与 RequestCache 交互,但作用于不同阶段:

  • SavedRequestAwareAuthenticationSuccessHandler 负责登录成功后的立即重定向(最常见场景)。
  • RequestCacheAwareFilter 负责登录成功后访问某个路径时,发现该路径与 SavedRequest 匹配则恢复请求上下文(如参数、请求头)。

登录后不能回到原页面的常见原因:① 自定义了 AuthenticationSuccessHandler 但没有委托 SavedRequestAwareAuthenticationSuccessHandler;② RequestCache 被配置为 NullRequestCache;③ 原请求是 AJAX 请求,被 HttpSessionRequestCache 的默认 Matcher 排除(默认不保存 AJAX);④ 原请求路径匹配了 permitAll(),没有被 ExceptionTranslationFilter 拦截,因此没有保存。

上一页
AnonymousAuthenticationFilter
下一页
ExceptionTranslationFilter