You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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";
  }
}
错误分析
  1. Filter冲突:formLogin()配置会自动添加默认的UsernamePasswordAuthenticationFilter,而你手动将自定义LocalAuthenticationFilter添加到默认Filter之前,导致两个同类型Filter共存,默认Filter优先处理请求但未使用你的自定义认证逻辑,最终SecurityContext未被正确更新。
  2. 自定义Filter未绑定登录请求:你的LocalAuthenticationFilter未指定处理的登录请求路径,默认UsernamePasswordAuthenticationFilter处理/login,若表单提交路径不匹配,自定义Filter不会触发。
  3. 未适配无密码认证场景:默认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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 16:32:05