
本文详解如何在 spring security 中正确实现强制密码修改的自定义 filter,重点解决因未排除目标页面导致的“too many redirects”问题,并提供可直接落地的完整配置与最佳实践。
本文详解如何在 spring security 中正确实现强制密码修改的自定义 filter,重点解决因未排除目标页面导致的“too many redirects”问题,并提供可直接落地的完整配置与最佳实践。
在基于 Spring Boot 的 Web 应用中,当业务要求用户首次登录或密码过期后必须立即跳转至密码重置页(如 /change-password),且禁止访问其他任何受保护资源时,常通过自定义 Filter 实现该拦截逻辑。但若实现不当(例如未对重置页本身放行),极易触发浏览器重定向循环(HTTP 302 → 302 → …),最终报错 “Too many redirects”。
核心问题在于:原始 ForcePasswordChangeFilter 在用户需改密时无条件重定向至 /change-password,但该请求本身仍会再次经过该 Filter —— 此时用户仍处于“已认证但需改密”状态,Filter 再次执行重定向,形成死循环。
✅ 正确做法是:在 Filter 内部显式放行 /change-password 路径,确保该页面可被正常访问,同时阻断其他所有受保护路径。
以下是优化后的完整实现:
✅ 1. 安全配置类(SecurityConfiguration.java)
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Autowired
private UserService userService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new AppUserDetailsService();
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(userDetailsService());
return provider;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/", "/css/**", "/js/**", "/assets/**", "/img/**", "/api/**").permitAll()
.requestMatchers("/change-password", "/change-password/**").permitAll() // ← 关键:显式放行改密页
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login.do")
.failureUrl("/login?error")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/home", true) // true: 强制跳转,避免 referer 回退
)
.logout(logout -> logout
.logoutUrl("/logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/login?logout")
);
// 将自定义 Filter 插入到 BasicAuthenticationFilter 之后(即认证完成之后)
http.addFilterAfter(forcePasswordChangeFilter(), BasicAuthenticationFilter.class);
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers("/resources/**", "/static/**", "/images/**"); // 静态资源忽略安全检查
}
@Bean
public ForcePasswordChangeFilter forcePasswordChangeFilter() {
return new ForcePasswordChangeFilter(userService);
}
}
✅ 2. 自定义过滤器(ForcePasswordChangeFilter.java)
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 (SecurityContextHolder.getContext().getAuthentication() != null
&& SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
User user = userService.authenticatedUser();
String path = request.getRequestURI();
// ✅ 关键逻辑:若用户需改密,且当前请求不是改密页,则重定向;否则放行
if (user != null && user.getShouldChangePassword()) {
if ("/change-password".equals(path) || path.startsWith("/change-password/")) {
filterChain.doFilter(request, response); // 放行改密页
return;
}
response.sendRedirect(request.getContextPath() + "/change-password");
return;
}
}
filterChain.doFilter(request, response);
}
}
⚠️ 注意事项:
- 使用 OncePerRequestFilter 替代 GenericFilter(更符合 Spring 生态习惯,自动避免重复执行);
- 优先通过 SecurityContextHolder 判断认证状态,比 userService.isAuthenticated() 更可靠;
- 重定向使用 response.sendRedirect(...) 时建议添加 request.getContextPath(),兼容部署上下文路径;
- /change-password/** 路径需在 authorizeHttpRequests 和 Filter 内双重放行(前者绕过权限校验,后者绕过强制跳转)。
✅ 3. 控制器补充(确保改密页无权限限制)
@Controller
public class AuthController {
@GetMapping("/change-password")
public String showChangePasswordForm(Model model) {
model.addAttribute("user", userService.authenticatedUser());
return "auth/change-password";
}
@PostMapping("/change-password")
public String changePassword(@RequestParam String newPassword,
RedirectAttributes redirectAttrs) {
try {
userService.updatePassword(newPassword);
redirectAttrs.addFlashAttribute("success", "密码修改成功!");
return "redirect:/home";
} catch (Exception e) {
redirectAttrs.addFlashAttribute("error", "密码更新失败:" + e.getMessage());
return "redirect:/change-password";
}
}
}
✅ 总结
强制密码修改功能看似简单,但极易因 Filter 执行时机与路径匹配疏漏引发重定向风暴。关键设计原则有三:
- 精准拦截时机:Filter 必须在认证完成后(如 BasicAuthenticationFilter 后)执行;
- 白名单兜底:所有与改密流程相关的路径(如 /change-password, /change-password/save)必须在 Filter 内和 Security 配置中同步放行;
- 幂等与健壮性:使用 OncePerRequestFilter、校验 Authentication 状态、处理空用户等边界情况。
遵循以上方案,即可稳定、安全地实现企业级强制密码策略。










