Spring自定义认证提供者:认证失败时返回自定义REST HTTP状态码
问题描述
我有一个可正常工作的自定义认证提供者:
@Component public class ApiAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(final Authentication authentication) throws AuthenticationException { final String name = authentication.getName(); final String password = authentication.getCredentials().toString(); if (isAuthorizedDevice(name, password)) { final List<GrantedAuthority> grantedAuths = new ArrayList<>(); grantedAuths.add(new SimpleGrantedAuthority(ApiInfo.Role.User)); final UserDetails principal = new User(name, password, grantedAuths); return new UsernamePasswordAuthenticationToken(principal, password, grantedAuths); } else { return null; } }
但认证失败时总是返回401状态码。我希望在暴力破解场景下返回429状态码,而非默认的401。我认为不应在认证提供者中处理,需在WebSecurityConfig配置中实现,但不知具体方法。
我已尝试抛出如下异常:
throw new LockedException("InvalidCredentialsFilter"); throw new AuthenticationCredentialsNotFoundException("Invalid Credentials!");
或注入响应对象并设置状态码:
response.setStatus(429);
但均无效,仍返回401。例如执行curl请求:
curl http://localhost:8080/api/v1.0/time --header "Authorization: Basic poaueiccrmpoawklerpo0i"
返回的响应体为:
{"timestamp":"2022-08-12T20:58:42.236+00:00","status":401,"error":"Unauthorized","path":"/api/v1.0/time"}
同时显示白标错误页:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Fri Aug 12 22:58:17 CEST 2022
There was an unexpected error (type=Unauthorized, status=401).
附:我的WebSecurityConfig配置:
@Configuration @EnableWebSecurity class WebSecurityConfig { AuthenticationProvider apiAuthenticationProvider; @Bean public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { return http .csrf().disable() .formLogin().disable() .httpBasic().and() .authenticationProvider(apiAuthenticationProvider) .authorizeRequests() .antMatchers(ApiInfo.BASE_URL + "/**") .fullyAuthenticated() .and() .build(); } }
解决方案
要实现暴力破解场景返回429,需要结合自定义认证异常和自定义AuthenticationEntryPoint,步骤如下:
1. 定义自定义认证异常
创建专属异常类标记"暴力破解"场景:
public class TooManyFailedAttemptsException extends AuthenticationException { public TooManyFailedAttemptsException(String msg) { super(msg); } }
2. 修改自定义认证提供者,添加失败次数检测
在ApiAuthenticationProvider中记录用户认证失败次数,超过阈值时抛出自定义异常(示例用本地缓存,分布式场景建议替换为Redis):
@Component public class ApiAuthenticationProvider implements AuthenticationProvider { // 本地缓存记录失败次数,1分钟后过期 private final LoadingCache<String, Integer> failedAttemptsCache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES) .build(new CacheLoader<String, Integer>() { @Override public Integer load(String key) { return 0; } }); @Override public Authentication authenticate(final Authentication authentication) throws AuthenticationException { final String name = authentication.getName(); final String password = authentication.getCredentials().toString(); // 检查失败次数是否超过阈值(示例设为5次) int attempts = failedAttemptsCache.getUnchecked(name); if (attempts >= 5) { throw new TooManyFailedAttemptsException("Too many failed attempts, please try again later."); } if (isAuthorizedDevice(name, password)) { // 认证成功,重置失败次数 failedAttemptsCache.invalidate(name); final List<GrantedAuthority> grantedAuths = new ArrayList<>(); grantedAuths.add(new SimpleGrantedAuthority(ApiInfo.Role.User)); final UserDetails principal = new User(name, password, grantedAuths); return new UsernamePasswordAuthenticationToken(principal, password, grantedAuths); } else { // 认证失败,累加失败次数 failedAttemptsCache.put(name, attempts + 1); throw new BadCredentialsException("Invalid credentials"); } } @Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } // 原有认证逻辑方法 private boolean isAuthorizedDevice(String name, String password) { // 你的认证实现 return false; } }
3. 自定义AuthenticationEntryPoint
替换HttpBasic默认入口点,根据异常类型返回对应状态码:
@Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { if (authException instanceof TooManyFailedAttemptsException) { response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS); response.setContentType("application/json"); response.getWriter().write("{\"timestamp\":\"" + LocalDateTime.now(ZoneOffset.UTC) + "\",\"status\":429,\"error\":\"Too Many Requests\",\"message\":\"" + authException.getMessage() + "\",\"path\":\"" + request.getRequestURI() + "\"}"); } else { // 其他认证异常返回默认401 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.getWriter().write("{\"timestamp\":\"" + LocalDateTime.now(ZoneOffset.UTC) + "\",\"status\":401,\"error\":\"Unauthorized\",\"message\":\"" + authException.getMessage() + "\",\"path\":\"" + request.getRequestURI() + "\"}"); } } }
4. 修改WebSecurityConfig配置
在SecurityFilterChain中配置自定义的AuthenticationEntryPoint:
@Configuration @EnableWebSecurity class WebSecurityConfig { private final AuthenticationProvider apiAuthenticationProvider; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; // 构造注入依赖 public WebSecurityConfig(AuthenticationProvider apiAuthenticationProvider, CustomAuthenticationEntryPoint customAuthenticationEntryPoint) { this.apiAuthenticationProvider = apiAuthenticationProvider; this.customAuthenticationEntryPoint = customAuthenticationEntryPoint; } @Bean public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { return http .csrf().disable() .formLogin().disable() .httpBasic(httpBasic -> httpBasic.authenticationEntryPoint(customAuthenticationEntryPoint)) // 配置自定义入口点 .authenticationProvider(apiAuthenticationProvider) .authorizeRequests() .antMatchers(ApiInfo.BASE_URL + "/**") .fullyAuthenticated() .and() .build(); } }
说明
- 分布式系统中,建议用Redis替代本地缓存存储失败次数,避免节点间数据不一致。
- 失败次数阈值、缓存过期时间可根据业务需求调整。
- 自定义EntryPoint直接返回JSON响应,避免触发白标错误页。
内容的提问来源于stack exchange,提问作者masterdany88
相关产品推荐
相关产品推荐

