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

Spring Security自定义认证提供者无限调用及异常拦截问题求助

问题:Spring Boot多认证提供者重复调用与异常处理问题

需求与已实现内容

  • 需求:开发Spring Boot应用,包含admin和company两个独立数据库实体,分别实现基于Spring Security的登录接口
  • 已实现:创建AdminAuthenticationProvider和CompanyAuthenticationProvider两个认证提供者,添加至AuthenticationManager,登录接口通过调用AuthenticationManager.authenticate()完成认证

已实现代码

AdminAuthenticationProvider.java

@Component
public class AdminAuthenticationProvider implements AuthenticationProvider {

   @Autowired
   CustomAdminDetailsService adminDetailsService;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      String username=String.valueOf(authentication.getPrincipal());
      String password=String.valueOf(authentication.getCredentials());
      
      UserDetails adminDetails=adminDetailsService.loadUserByUsername(username);
      if(adminDetails!=null){
        if(passwordEncoder.matches(password, adminDetails.getPassword())){
            UsernamePasswordAuthenticationToken token=new UsernamePasswordAuthenticationToken(username, password,new ArrayList<>());
            return token;
        }
      }

      throw new BadCredentialsException("Wrong Credentials");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }
    
}

CompanyAuthenticationProvider.java

@Component
public class CompanyAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private CustomCompanyDetailsService companyDetailsService;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = String.valueOf(authentication.getPrincipal());
        String password = String.valueOf(authentication.getCredentials());

        UserDetails companyDetails = companyDetailsService.loadUserByUsername(username);
        if (companyDetails != null) {
            if (passwordEncoder.matches(password, companyDetails.getPassword())) {
                UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password,
                        new ArrayList<>());
                return token;
            }
        }

        throw new BadCredentialsException("Wrong Credentials");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }

}

SecurityConfigs.java

@Configuration
@EnableWebSecurity
@EnableWebMvc 
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfigs {
    public static final String[] PUBLIC_URLS = {
        "/admin/register",
        "/admin/login",
        "/company/login",
        "/email/**"
    };

    @Autowired
    private AuthenticationEntryPoint unauthorizedHandler;
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    @Autowired
    CompanyAuthenticationProvider companyAuthenticationProvider;

    @Autowired
    AdminAuthenticationProvider adminAuthenticationProvider;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http,AuthenticationManager authenticationManager) throws Exception {
        http.csrf().disable()
                .authorizeHttpRequests()
                .requestMatchers(PUBLIC_URLS)
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterBefore(this.jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        http.authenticationManager(authenticationManager);
        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = http
                .getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder
        .authenticationProvider(adminAuthenticationProvider);

        return authenticationManagerBuilder.build();
    }
}

JwtAuthenticationEntryPoint.java

public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint{
   
    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
                resolver.resolveException(request, response, null, authException);
    }
    
}

遇到的问题

  1. 用户名或密码错误时,认证提供者被无限重复调用(已在提供者中抛出BadCredentialsException)
  2. 无法拦截BadCredentialsException,无法向用户返回明确的"用户名或密码错误"提示

解决方案

问题1:认证提供者无限重复调用

原因

当AuthenticationManager同时持有多个认证提供者时,默认会依次调用每个提供者直到认证成功或全部失败。若所有提供者都抛出异常,加上JwtAuthenticationFilter或异常处理逻辑配置不当,会触发循环调用。

解决方法

为不同登录接口绑定专属的认证管理器,避免跨类型认证尝试:

  1. 在SecurityConfigs中创建两个独立的AuthenticationManager:
@Bean
public AuthenticationManager adminAuthenticationManager(HttpSecurity http) throws Exception {
    AuthenticationManagerBuilder authBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
    authBuilder.authenticationProvider(adminAuthenticationProvider);
    return authBuilder.build();
}

@Bean
public AuthenticationManager companyAuthenticationManager(HttpSecurity http) throws Exception {
    AuthenticationManagerBuilder authBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
    authBuilder.authenticationProvider(companyAuthenticationProvider);
    return authBuilder.build();
}
  1. 在登录控制器中注入对应类型的认证管理器:
@RestController
@RequestMapping("/admin")
public class AdminLoginController {
    @Autowired
    private AuthenticationManager adminAuthenticationManager;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            Authentication auth = adminAuthenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
            );
            // 生成JWT等后续逻辑
            return ResponseEntity.ok("Admin登录成功");
        } catch (Exception e) {
            // 捕获异常返回错误信息
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误");
        }
    }
}

@RestController
@RequestMapping("/company")
public class CompanyLoginController {
    @Autowired
    private AuthenticationManager companyAuthenticationManager;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            Authentication auth = companyAuthenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
            );
            // 生成JWT等后续逻辑
            return ResponseEntity.ok("Company登录成功");
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误");
        }
    }
}
  1. 检查JwtAuthenticationFilter逻辑,确保不对登录接口的请求做额外认证处理,避免触发循环调用。

问题2:拦截BadCredentialsException返回友好提示

方法1:控制器层直接捕获异常

如上述登录控制器示例,在调用authenticate()时用try-catch捕获BadCredentialsException,直接返回自定义错误响应。

方法2:全局异常处理器

创建全局异常处理器统一处理所有认证相关异常:

@RestControllerAdvice
public class GlobalAuthenticationExceptionHandler {

    @ExceptionHandler(BadCredentialsException.class)
    public ResponseEntity<ErrorResponse> handleBadCredentials(BadCredentialsException ex) {
        ErrorResponse response = new ErrorResponse(HttpStatus.UNAUTHORIZED.value(), "用户名或密码错误");
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
    }

    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<ErrorResponse> handleAuthenticationException(AuthenticationException ex) {
        ErrorResponse response = new ErrorResponse(HttpStatus.UNAUTHORIZED.value(), ex.getMessage());
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
    }

    // 错误响应实体类
    static class ErrorResponse {
        private int code;
        private String message;

        public ErrorResponse(int code, String message) {
            this.code = code;
            this.message = message;
        }

        // getter和setter方法
        public int getCode() { return code; }
        public void setCode(int code) { this.code = code; }
        public String getMessage() { return message; }
        public void setMessage(String message) { this.message = message; }
    }
}

注:你的JwtAuthenticationEntryPoint已配置用HandlerExceptionResolver处理异常,全局处理器会自动接管这类异常的响应生成。


内容的提问来源于stack exchange,提问作者ayush

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 05:27:02