
本文详解如何在 Spring Security 中正确实现强制密码修改的自定义 Filter,避免“Too many redirects”循环重定向问题,关键在于排除 /change-password 路径自身,确保用户能正常访问密码重置页面。
本文详解如何在 spring security 中正确实现强制密码修改的自定义 filter,避免“too many redirects”循环重定向问题,关键在于排除 `/change-password` 路径自身,确保用户能正常访问密码重置页面。
在基于 Spring Boot 的 Web 应用中,当业务要求用户首次登录或密码过期后必须立即修改密码时,仅靠权限配置无法满足“除密码页外禁止访问任何受保护资源”的强约束。此时需引入自定义 Filter,在认证成功后的请求链中动态拦截并重定向。但若处理不当(如未豁免目标页面),极易触发浏览器重定向循环(HTTP 302 → /change-password → 再次被 Filter 拦截 → 再次重定向……),最终报错 “Too Many Redirects”。
✅ 正确实现要点
核心原则:Filter 必须识别并放行 /change-password 自身请求,否则每次访问该路径都会被重复拦截,形成死循环。
以下是优化后的 ForcePasswordChangeFilter 实现:
public class ForcePasswordChangeFilter extends OncePerRequestFilter {
private final UserService userService;
public ForcePasswordChangeFilter(UserService userService) {
this.userService = userService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 仅对已认证用户生效
if (userService.isAuthenticated()) {
User user = userService.authenticatedUser();
String requestPath = request.getRequestURI();
// 关键:明确放行密码修改页,避免递归重定向
if (user.getShouldChangePassword() && !"/change-password".equals(requestPath)) {
response.sendRedirect("/change-password");
return;
}
}
// 其他情况(未认证、无需改密、已是改密页)均放行
filterChain.doFilter(request, response);
}
}
? 注意替换基类:推荐继承 OncePerRequestFilter(而非 GenericFilter),它能确保每个请求只执行一次,避免在异步或包含请求转发的场景中重复触发。
? 配置与集成说明
-
Filter 注册位置要合理:
在 SecurityFilterChain 中,应将该 Filter 插入到认证完成之后、授权检查之前的位置。使用 addFilterAfter(..., UsernamePasswordAuthenticationFilter.class) 更稳妥(因 BasicAuthenticationFilter 可能不适用表单登录场景):@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/login", "/", "/css/**", "/js/**", "/api/**", "/change-password").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .loginProcessingUrl("/login.do") .defaultSuccessUrl("/home", true) // true 表示始终跳转,忽略原始请求 .failureUrl("/login?error") ) .logout(logout -> logout .logoutUrl("/logout") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .logoutSuccessUrl("/login?logout") ); // ✅ 插入时机:在表单认证之后、其他授权过滤器之前 http.addFilterAfter(forcePasswordChangeFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } 静态资源与 API 接口仍需显式放行:
WebSecurityCustomizer 中的 ignoring() 仅绕过整个 Security 过滤器链,不影响 ForcePasswordChangeFilter(因其是手动添加的)。因此务必在 authorizeHttpRequests 中也将 /change-password 加入 permitAll(),确保未登录用户不会被误拦(例如从书签直接访问)。-
Controller 端配合建议:
- /change-password 的 GET 和 POST 接口均需保持公开(已通过 permitAll() 保障);
- 提交成功后,建议清除 shouldChangePassword 标志并更新密码,再重定向至主页,避免下次登录再次触发。
⚠️ 常见陷阱总结
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| Too many redirects | Filter 未排除 /change-password,导致重定向自循环 | 在 Filter 内判断 requestURI,匹配则 chain.doFilter() |
| 用户登录后仍可访问 /home | getShouldChangePassword() 返回 false 或 userService.isAuthenticated() 判断不准 | 确保 UserService 正确从 SecurityContext 获取 Authentication 并校验 isAuthenticated() |
| 密码修改页样式/JS 加载失败 | /change-password 被拦截,但其依赖的 /css/**、/js/** 未在 permitAll() 中声明 | 在 authorizeHttpRequests 中一并放行静态资源路径 |
通过以上结构化实现,你将获得一个健壮、无副作用的强制密码修改机制——既满足安全合规要求,又保障用户体验流畅。










