在最近的微服务项目开发中,我遇到了一个非常经典的 Spring Security 依赖冲突问题。
项目的基本架构是:一个负责校验密码和颁发 Token 的 auth-server,以及多个负责处理业务逻辑的资源服务(如 manager-server)。为了避免每个业务微服务都写一遍 JWT 拦截器和异常处理代码,我抽取了一个公共的 common-core 模块,并在里面写了全局的 Security 配置类。
结果,当 auth-server 引入 common-core 时,项目直接启动失败报 NoUniqueBeanDefinitionException。
这篇文章记录了我是如何通过将其重构成一个动态开关的 Spring Boot Starter,最终优雅解决这个冲突的。
💥 冲突重现:为什么会启动失败?
最初,我在 common-core 里写了一个 ResourceServerConfig 类,加上了 @Configuration 注解并注入了 SecurityFilterChain Bean。 同时,我的 auth-server 作为一个特殊的微服务,它不需要 JWT 拦截(因为它本身就是发 Token 的),但它需要暴露 /login 接口并进行账号密码校验,所以我又在 auth-server 里写了一个 AuthSecurityConfig,同样也注入了 SecurityFilterChain Bean。
报错原因: 当 auth-server 启动并扫描到引入的 common-core 依赖时,Spring 容器中同时出现了两个 SecurityFilterChain Bean。在没有明确指定优先级的情况下,Spring Security 直接宕机,抛出 NoUniqueBeanDefinitionException。
🛠️ 破局思路:抽离 Starter 与条件装配
要解决这个问题,我最初的想法是每次新建微服务手动把过滤器的代码复制一遍。但这显然违背了 DRY (Don't Repeat Yourself) 原则。
最终我采用了更企业级的方案:将 common-core 彻底改造为一个 Spring Boot Starter,利用 @ConditionalOnProperty 条件注解,让业务微服务默认开启拦截,而网关和 Auth 服务可以通过 YML 明确拒绝拦截。
步骤一:抽取动态属性配置 (支持 YML 白名单)
为了让各个微服务不仅能使用统一的拦截规则,还能配置自己的特殊白名单,我新建了一个属性配置类:
package cn.okcl.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* 安全模块动态配置属性类
*/
@Data
@ConfigurationProperties(prefix = "wild.security")
public class WildSecurityProperties {
/**
* 是否开启安全配置(总开关)
*/
private Boolean enable = true;
/**
* 各微服务自定义的放行白名单路径集合
*/
private List<String> ignoreUrls = new ArrayList<>();
}
步骤二:统一常量维护基础资源
对于一些公共不需要拦截的静态资源(如 Swagger 文档),我将其抽离到专门的常量类中,保证所有微服务的底层放行规则一致:
package cn.okcl.constant;
/**
* 资源服务器常量
*/
public class ResourceConstants {
/**
* 允许匿名访问的通用资源路径(不需要带 Token 即可访问)
*/
public static final String[] RESOURCE_PATH = {
"/doc.html",
"/webjars/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/favicon.ico"
};
}
步骤三:编写终极自动装配类 (核心引擎)
这是最核心的一步。我将原来的配置类重命名为 WildSecurityAutoConfiguration,并打上了 @ConditionalOnProperty 注解,这就是解决冲突的“绝杀技”。代码如下:
package cn.okcl.config;
import cn.okcl.constant.ResourceConstants;
import cn.okcl.filter.JwtAuthenticationTokenFilter;
import cn.okcl.handle.LoginUnAccessDeniedHandler;
import cn.okcl.handle.LoginUnAuthenticationEntryPointHandler;
import jakarta.annotation.Resource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级权限注解
// 启用动态属性配置
@EnableConfigurationProperties(WildSecurityProperties.class)
// 【绝杀技】:当 YML 中 wild.security.enable = false 时本类失效,缺失时默认为 true
@ConditionalOnProperty(prefix = "wild.security", name = "enable", havingValue = "true", matchIfMissing = true)
public class WildSecurityAutoConfiguration {
// 注入我们刚才写的属性类,用来拿 YML 里的自定义白名单
@Resource
private WildSecurityProperties securityProperties;
@Resource
private LoginUnAccessDeniedHandler loginUnAccessDeniedHandler;
@Resource
private LoginUnAuthenticationEntryPointHandler loginUnAuthenticationEntryPointHandler;
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 1. 准备基础的框架级放行路径
List<String> permitUrls = new ArrayList<>(Arrays.asList(ResourceConstants.RESOURCE_PATH));
// 2. 把当前微服务 YML 里的自定义放行路径加进来(如果有的话)
if (securityProperties.getIgnoreUrls() != null && !securityProperties.getIgnoreUrls().isEmpty()) {
permitUrls.addAll(securityProperties.getIgnoreUrls());
}
http.cors(AbstractHttpConfigurer::disable) // 禁用跨域
.csrf(AbstractHttpConfigurer::disable) // 禁用csrf
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
// 3. 将合并后的路径数组交给 Security 放行
.requestMatchers(permitUrls.toArray(new String[0])).permitAll()
.anyRequest().authenticated()
);
// 添加jwt认证过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置异常处理
http.exceptionHandling(exception -> exception
.accessDeniedHandler(loginUnAccessDeniedHandler)
.authenticationEntryPoint(loginUnAuthenticationEntryPointHandler)
);
return http.build();
}
}
步骤四:配置 Spring Boot 3 自动装配指路牌
由于项目使用的是 Spring Boot 3,spring.factories 机制已经被废弃。需要在 common-core 的 src/main/resources 目录下创建 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。
文件内容只需填入配置类的全限定名:
cn.okcl.config.WildSecurityAutoConfiguration
🚀 最终的微服务调用体验
经过这样的 Starter 封装后,微服务之间的安全依赖变得非常清爽:
1. 业务服务(如 manager-server)直接躺赢 新建一个业务模块,只需引入 common-core 的 POM 依赖。由于 @ConditionalOnProperty 的 matchIfMissing = true 属性,安全配置默认直接生效,所有接口自动被 JWT 拦截保护,无需编写一行 Java 代码。如果需要放行部分接口,只需在对应微服务的 Nacos 配置(或 YML)中加入:
wild:
security:
ignore-urls:
- /api/public/news/list
2. Auth 服务完美脱身 在 auth-server 或者 Spring Cloud Gateway 的配置文件中,加入以下配置:
wild:
security:
enable: false
这样就能彻底关闭 common-core 送来的安全拦截规则。auth-server 可以安安心心地保留自己内部的那个专门用来处理 /login 的 SecurityFilterChain,再也不会发生 Bean 冲突。
总结
在微服务架构下,将具有普适性的拦截器和异常处理机制下沉到基础模块是非常有必要的。利用 @ConditionalOnProperty 和自动装配机制来构建自定义 Starter,不仅解决了多模块间的依赖冲突,还极大地提升了新模块的接入效率,体现了良好的“对扩展开放,对修改关闭”的设计原则。