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() 内部逻辑:
- 从
RequestCache获取SavedRequest。 - 如果存在,构造重定向 URL(原始请求路径)。
- 调用
RequestCache.removeRequest()清理缓存。 - 如果不存在
SavedRequest,使用defaultTargetUrl。
面试考点:
RequestCacheAwareFilter与SavedRequestAwareAuthenticationSuccessHandler的关系是什么?为什么有时候登录后不能正确回到原页面? 答:RequestCacheAwareFilter在登录成功后的第一次请求中检查RequestCache,如果存在SavedRequest则恢复原始请求。而SavedRequestAwareAuthenticationSuccessHandler在登录提交成功时直接从RequestCache获取被保存的请求并进行重定向。两者都与RequestCache交互,但作用于不同阶段:
SavedRequestAwareAuthenticationSuccessHandler负责登录成功后的立即重定向(最常见场景)。RequestCacheAwareFilter负责登录成功后访问某个路径时,发现该路径与SavedRequest匹配则恢复请求上下文(如参数、请求头)。登录后不能回到原页面的常见原因:① 自定义了
AuthenticationSuccessHandler但没有委托SavedRequestAwareAuthenticationSuccessHandler;②RequestCache被配置为NullRequestCache;③ 原请求是 AJAX 请求,被HttpSessionRequestCache的默认 Matcher 排除(默认不保存 AJAX);④ 原请求路径匹配了permitAll(),没有被ExceptionTranslationFilter拦截,因此没有保存。