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); } }
遇到的问题
- 用户名或密码错误时,认证提供者被无限重复调用(已在提供者中抛出
BadCredentialsException) - 无法拦截
BadCredentialsException,无法向用户返回明确的"用户名或密码错误"提示
解决方案
问题1:认证提供者无限重复调用
原因
当AuthenticationManager同时持有多个认证提供者时,默认会依次调用每个提供者直到认证成功或全部失败。若所有提供者都抛出异常,加上JwtAuthenticationFilter或异常处理逻辑配置不当,会触发循环调用。
解决方法
为不同登录接口绑定专属的认证管理器,避免跨类型认证尝试:
- 在
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(); }
- 在登录控制器中注入对应类型的认证管理器:
@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("用户名或密码错误"); } } }
- 检查
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
相关产品推荐
相关产品推荐

