Spring Security 6.3.1中自定义UsernamePasswordAuthenticationFilter的实现问题
问题描述
正在重构基于Java 8、旧版Spring Boot和Spring Security的应用,当前升级到Spring Boot 3.3.2 + Spring Security 6.3.1。本地版本的登录逻辑要求:
- 仅通过username字段认证,用户名不存在则自动存入数据库,默认赋予
ROLE_USER角色,部分用户带有ROLE_ADMIN角色 - 已自定义
LocalAuthenticationFilter继承UsernamePasswordAuthenticationFilter,并实现了自定义AuthenticationManager生成UsernamePasswordAuthenticationToken,但认证后SecurityContext中始终没有该令牌。
相关代码如下:
自定义认证Filter
public class LocalAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public LocalAuthenticationFilter(UserService userService) { super(); setAuthenticationManager(new LocalAuthenticationManager(userService)); } public static class LocalAuthenticationManager implements AuthenticationManager { private final UserService userService; public LocalAuthenticationManager(UserService personService) { this.userService = personService; } @Override public Authentication authenticate(Authentication authentication) { String username = authentication.getName(); if (!StringUtils.hasText(username)) { throw new BadCredentialsException("Username is empty"); } Set<GrantedAuthority> mappedAuthorities = new HashSet<>(); mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); User user = userService.findByUid(username); // 不存在则自动存入数据库 if (user.isAdmin()) { mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } return new UsernamePasswordAuthenticationToken(username, "", mappedAuthorities); } } }
安全配置类
@Configuration @EnableWebSecurity public class SecurityConfig { private static final String[] AUTH_ALLOWED = { // 允许匿名访问的端点 }; private final UserService userService; @Autowired public SecurityConfig( UserService userService) { this.userService = userService; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests( (authorize) -> authorize.requestMatchers(AUTH_ALLOWED).permitAll().anyRequest().authenticated()) .addFilterBefore( new LocalAuthenticationFilter(userService), UsernamePasswordAuthenticationFilter.class) .formLogin(form -> form.loginPage("/").permitAll()) .logout(LogoutConfigurer::permitAll); return http.build(); } }
控制器代码
@Controller @RequestMapping("/") public class MainController { @GetMapping public String index(Authentication authentication) { SecurityContext securityContext = SecurityContextHolder.getContext(); if (authentication == null || securityContext.getAuthentication() instanceof AnonymousAuthenticationToken) { return "login"; } return "index"; } }
错误分析
- Filter冲突:
formLogin()配置会自动添加默认的UsernamePasswordAuthenticationFilter,而你手动将自定义LocalAuthenticationFilter添加到默认Filter之前,导致两个同类型Filter共存,默认Filter优先处理请求但未使用你的自定义认证逻辑,最终SecurityContext未被正确更新。 - 自定义Filter未绑定登录请求:你的
LocalAuthenticationFilter未指定处理的登录请求路径,默认UsernamePasswordAuthenticationFilter处理/login,若表单提交路径不匹配,自定义Filter不会触发。 - 未适配无密码认证场景:默认
UsernamePasswordAuthenticationFilter会同时获取username和password参数,但你的场景不需要密码验证,未重写attemptAuthentication方法导致逻辑不匹配。
解决方案
步骤1:修正自定义Filter逻辑
重写attemptAuthentication方法,只读取username参数,忽略密码,并指定登录请求路径:
public class LocalAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public LocalAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); // 设置登录请求处理路径,需与表单提交action一致 setFilterProcessesUrl("/login"); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 仅读取username参数 String username = obtainUsername(request); username = (username != null) ? username.trim() : ""; // 构造未认证令牌,密码传空字符串 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, ""); setDetails(request, authRequest); return getAuthenticationManager().authenticate(authRequest); } public static class LocalAuthenticationManager implements AuthenticationManager { private final UserService userService; public LocalAuthenticationManager(UserService userService) { this.userService = userService; } @Override public Authentication authenticate(Authentication authentication) { String username = authentication.getName(); if (!StringUtils.hasText(username)) { throw new BadCredentialsException("Username is empty"); } Set<GrantedAuthority> mappedAuthorities = new HashSet<>(); mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); User user = userService.findByUid(username); // 不存在则自动存入 if (user.isAdmin()) { mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } // 返回已认证令牌,密码字段无意义可传空 return new UsernamePasswordAuthenticationToken(username, "", mappedAuthorities); } } }
步骤2:调整安全配置,替换默认Filter
移除addFilterBefore,改用addFilterAt替换默认的UsernamePasswordAuthenticationFilter,并确保formLogin配置与Filter路径一致:
@Configuration @EnableWebSecurity public class SecurityConfig { private static final String[] AUTH_ALLOWED = { // 允许匿名访问的端点 }; private final UserService userService; public SecurityConfig(UserService userService) { this.userService = userService; } @Bean public AuthenticationManager localAuthenticationManager() { return new LocalAuthenticationFilter.LocalAuthenticationManager(userService); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize .requestMatchers(AUTH_ALLOWED).permitAll() .anyRequest().authenticated()) // 替换默认的UsernamePasswordAuthenticationFilter为自定义实现 .addFilterAt(new LocalAuthenticationFilter(localAuthenticationManager()), UsernamePasswordAuthenticationFilter.class) .formLogin(form -> form .loginPage("/") // 自定义登录页面路径 .loginProcessingUrl("/login") // 表单提交路径,需与Filter配置一致 .permitAll()) .logout(LogoutConfigurer::permitAll); return http.build(); } }
步骤3:检查登录表单
确保登录表单的提交路径为/login,用户名输入框的name属性为username:
<!-- 示例登录表单 --> <form action="/login" method="post"> <input type="text" name="username" placeholder="输入用户名"/> <button type="submit">登录</button> </form>
验证逻辑
登录成功后,SecurityContextHolder.getContext().getAuthentication()会返回你生成的UsernamePasswordAuthenticationToken,控制器中的Authentication参数也会被正确注入,此时会跳转到index页面。
内容的提问来源于stack exchange,提问作者Bali9030
相关产品推荐
相关产品推荐

