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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 07:24:22