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

如何在Spring Security 5.8.7中结合默认配置使用自定义BasicAuthenticationFilter

问题描述

我正在使用Spring Security 5.8.7,当前的过滤器链配置如下:

@Bean
public SecurityFilterChain remotingFilterChain( HttpSecurity http ) throws Exception {
    return http
            .securityMatcher( "/remoting/**" )
            .authorizeHttpRequests()
                    .anyRequest().authenticated()
                    .and()
            .httpBasic()
                    .and()
            .anonymous()
                    .and()
            .x509()
                    .subjectPrincipalRegex( "CN=(.*?)(?:,|$)" )
                    .and()
            .csrf()
                    .disable()
            .sessionManagement()
                    .disable()
            .build();
}

现在我希望通过重写以下两个方法来自定义BasicAuthenticationFilter:

@Override
protected String getCredentialsCharset( HttpServletRequest httpRequest ) {
    // 根据条件切换字符集
}

@Override
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException failed) throws IOException {
    // 增加登录失败次数
}

原本的思路是实现自定义过滤器类:

class MyBasicAuthenticationFilter extends BasicAuthenticationFilter {
    ...
}

并调用:

.addFilterAt( new MyBasicAuthenticationFilter(), BasicAuthenticationFilter.class )

但我不想丢失http.httpBasic()提供的默认配置,而HttpBasicConfigurer是final类,无法扩展并覆盖其configure()方法(该方法中会创建BasicAuthenticationFilter实例)。

另一种思路是使用:

.httpBasic()
        .withObjectPostProcessor( myPostProcessor )
        .and()

增强原生创建的BasicAuthenticationFilter,但BasicAuthenticationFilter不是接口,无法使用java.lang.reflect.Proxy模式。

请问如何才能结合简单的默认配置使用自定义的BasicAuthenticationFilter?


解决方案

你可以通过以下两种方式实现,既保留httpBasic()的默认配置,又实现自定义逻辑:

方式一:复用默认配置参数初始化自定义过滤器

先借助HttpBasicConfigurer生成默认配置,提取其中的核心依赖(如AuthenticationManager、BasicAuthenticationEntryPoint),用这些依赖初始化自定义过滤器,确保默认配置不丢失:

@Bean
public SecurityFilterChain remotingFilterChain(HttpSecurity http) throws Exception {
    // 获取httpBasic的默认配置器
    HttpBasicConfigurer<HttpSecurity> httpBasicConfigurer = http.httpBasic();
    // 从共享对象中获取AuthenticationManager,这是BasicAuthenticationFilter的必要依赖
    AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
    
    // 初始化自定义过滤器
    MyBasicAuthenticationFilter customBasicFilter = new MyBasicAuthenticationFilter(authenticationManager);
    
    // 复制默认的认证入口点到自定义过滤器
    BasicAuthenticationEntryPoint defaultEntryPoint = httpBasicConfigurer.getEntryPoint();
    customBasicFilter.setAuthenticationEntryPoint(defaultEntryPoint);
    
    // 复制默认的字符集配置,作为自定义逻辑的 fallback
    String defaultCharset = httpBasicConfigurer.getCredentialsCharset();
    customBasicFilter.setDefaultCredentialsCharset(defaultCharset);

    return http
            .securityMatcher("/remoting/**")
            .authorizeHttpRequests()
                .anyRequest().authenticated()
                .and()
            // 调用httpBasic()保留默认配置
            .httpBasic()
                .and()
            .anonymous()
                .and()
            .x509()
                .subjectPrincipalRegex("CN=(.*?)(?:,|$)")
                .and()
            .csrf().disable()
            .sessionManagement().disable()
            // 替换原生过滤器为自定义实现
            .addFilterAt(customBasicFilter, BasicAuthenticationFilter.class)
            .build();
}

// 自定义过滤器实现
class MyBasicAuthenticationFilter extends BasicAuthenticationFilter {
    private String defaultCredentialsCharset;

    public MyBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected String getCredentialsCharset(HttpServletRequest httpRequest) {
        // 自定义字符集切换逻辑,默认使用配置的默认值
        String requestCharset = httpRequest.getHeader("X-Credentials-Charset");
        return StringUtils.hasText(requestCharset) ? requestCharset : this.defaultCredentialsCharset;
    }

    @Override
    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                                AuthenticationException failed) throws IOException {
        // 执行登录失败次数统计逻辑
        // loginFailureService.recordFailure(request.getRemoteAddr());
        
        // 调用父类方法,保留默认的失败响应逻辑(如返回401)
        super.onUnsuccessfulAuthentication(request, response, failed);
    }

    public void setDefaultCredentialsCharset(String defaultCredentialsCharset) {
        this.defaultCredentialsCharset = defaultCredentialsCharset;
    }
}

方式二:用装饰器模式包装原生过滤器

通过ObjectPostProcessor获取原生BasicAuthenticationFilter实例,用装饰器类包装它,重写需要自定义的方法,其他逻辑委托给原生过滤器:

@Bean
public SecurityFilterChain remotingFilterChain(HttpSecurity http) throws Exception {
    return http
            .securityMatcher("/remoting/**")
            .authorizeHttpRequests()
                .anyRequest().authenticated()
                .and()
            .httpBasic()
                .withObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() {
                    @Override
                    public <O extends BasicAuthenticationFilter> O postProcess(O filter) {
                        // 用装饰器包装原生过滤器
                        return (O) new DecoratedBasicAuthenticationFilter(filter);
                    }
                })
                .and()
            .anonymous()
                .and()
            .x509()
                .subjectPrincipalRegex("CN=(.*?)(?:,|$)")
                .and()
            .csrf().disable()
            .sessionManagement().disable()
            .build();
}

// 装饰器类,包装原生过滤器
class DecoratedBasicAuthenticationFilter extends BasicAuthenticationFilter {
    private final BasicAuthenticationFilter delegate;

    public DecoratedBasicAuthenticationFilter(BasicAuthenticationFilter delegate) {
        super(delegate.getAuthenticationManager());
        this.delegate = delegate;
        // 复制原生过滤器的认证入口点
        this.setAuthenticationEntryPoint(delegate.getAuthenticationEntryPoint());
    }

    @Override
    protected String getCredentialsCharset(HttpServletRequest httpRequest) {
        // 自定义字符集逻辑
        String customCharset = httpRequest.getHeader("X-Credentials-Charset");
        return StringUtils.hasText(customCharset) ? customCharset : delegate.getCredentialsCharset(httpRequest);
    }

    @Override
    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                                AuthenticationException failed) throws IOException {
        // 执行登录失败次数统计逻辑
        // loginFailureService.recordFailure(request.getRemoteAddr());
        
        // 委托给原生过滤器处理默认失败逻辑
        delegate.onUnsuccessfulAuthentication(request, response, failed);
    }

    // 其他核心方法直接委托给原生过滤器
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        delegate.doFilterInternal(request, response, chain);
    }
}

两种方式都能保留httpBasic()的默认配置(如默认的认证入口、字符集等),同时实现自定义的方法逻辑。方式一更直观,方式二通过装饰器降低了对原生代码的侵入性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 17:03:11