如何在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

