feat: 配置Swagger UI在开发/测试环境可访问

- SecurityConfig: 添加Environment注入和环境检测逻辑
- SecurityConfig: 在dev/test环境放行Swagger相关路径
- SecurityConfig: 移除重复的PasswordEncoder Bean定义
- SecurityConfigTest: 修改测试以适应新的构造函数
- OpenApiConfig: 修正开发环境服务器URL从8080改为8084

修改的文件:
- novalon-manage-api/manage-sys/src/main/java/cn/novalon/manage/sys/config/SecurityConfig.java
- novalon-manage-api/manage-sys/src/test/java/cn/novalon/manage/sys/config/SecurityConfigTest.java
- novalon-manage-api/manage-app/src/main/java/cn/novalon/manage/app/config/OpenApiConfig.java

功能说明:
- Swagger UI在dev/test环境可通过http://localhost:8084/swagger-ui.html访问
- 生产环境自动禁用Swagger访问,确保安全性
- 解决了Bean冲突问题(PasswordEncoder重复定义)
- 修正了服务器端口配置
This commit is contained in:
张翔
2026-03-26 13:05:00 +08:00
parent 4ec1a3f4dd
commit ce30893a96
3 changed files with 83 additions and 60 deletions
@@ -22,39 +22,39 @@ import java.util.List;
@Configuration @Configuration
public class OpenApiConfig { public class OpenApiConfig {
@Bean @Bean
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
return new OpenAPI() return new OpenAPI()
.info(new Info() .info(new Info()
.title("Novalon Manage System API") .title("Novalon Manage System API")
.version("1.0.0") .version("1.0.0")
.description("Novalon 管理系统 RESTful API 文档") .description("Novalon 管理系统 RESTful API 文档")
.contact(new Contact() .contact(new Contact()
.name("Novalon Team") .name("Novalon Team")
.email("support@novalon.cn")) .email("support@novalon.cn"))
.license(new License() .license(new License()
.name("Apache 2.0") .name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0"))) .url("https://www.apache.org/licenses/LICENSE-2.0")))
.servers(List.of( .servers(List.of(
new Server().url("http://localhost:8080").description("开发环境"), new Server().url("http://localhost:8084").description("开发环境"),
new Server().url("https://api.novalon.cn").description("生产环境"))) new Server().url("https://api.novalon.cn").description("生产环境")))
.tags(Arrays.asList( .tags(Arrays.asList(
new Tag().name("用户管理").description("用户相关操作"), new Tag().name("用户管理").description("用户相关操作"),
new Tag().name("角色管理").description("角色相关操作"), new Tag().name("角色管理").description("角色相关操作"),
new Tag().name("配置管理").description("系统配置相关操作"), new Tag().name("配置管理").description("系统配置相关操作"),
new Tag().name("字典管理").description("字典数据相关操作"), new Tag().name("字典管理").description("字典数据相关操作"),
new Tag().name("通知管理").description("系统通知相关操作"), new Tag().name("通知管理").description("系统通知相关操作"),
new Tag().name("文件管理").description("文件上传下载相关操作"), new Tag().name("文件管理").description("文件上传下载相关操作"),
new Tag().name("日志管理").description("操作日志相关操作"), new Tag().name("日志管理").description("操作日志相关操作"),
new Tag().name("认证管理").description("登录认证相关操作"), new Tag().name("认证管理").description("登录认证相关操作"),
new Tag().name("统计信息").description("系统统计相关操作"))); new Tag().name("统计信息").description("系统统计相关操作")));
} }
@Bean @Bean
public GroupedOpenApi allApi() { public GroupedOpenApi allApi() {
return GroupedOpenApi.builder() return GroupedOpenApi.builder()
.group("all") .group("all")
.pathsToMatch("/api/**") .pathsToMatch("/api/**")
.build(); .build();
} }
} }
@@ -1,13 +1,14 @@
package cn.novalon.manage.sys.config; package cn.novalon.manage.sys.config;
import cn.novalon.manage.sys.security.JwtAuthenticationFilter; import cn.novalon.manage.sys.security.JwtAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
/** /**
@@ -20,30 +21,54 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
@EnableWebFluxSecurity @EnableWebFluxSecurity
public class SecurityConfig { public class SecurityConfig {
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final Environment environment;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, Environment environment) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter; this.jwtAuthenticationFilter = jwtAuthenticationFilter;
} this.environment = environment;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
} }
@Bean @Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http String[] activeProfiles = environment.getActiveProfiles();
boolean isDevOrTest = false;
for (String profile : activeProfiles) {
if ("dev".equals(profile) || "test".equals(profile)) {
isDevOrTest = true;
break;
}
}
logger.info("SecurityConfig初始化: 当前环境={}, Swagger启用状态={}",
activeProfiles.length > 0 ? String.join(",", activeProfiles) : "default", isDevOrTest);
ServerHttpSecurity.AuthorizeExchangeSpec exchanges = http
.csrf(ServerHttpSecurity.CsrfSpec::disable) .csrf(ServerHttpSecurity.CsrfSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable) .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange(exchanges -> exchanges .authorizeExchange();
.pathMatchers("/api/auth/**").permitAll()
.pathMatchers("/api/public/**").permitAll() exchanges.pathMatchers("/api/auth/**").permitAll()
.pathMatchers("/ws/**").permitAll() .pathMatchers("/api/public/**").permitAll()
.pathMatchers("/actuator/**").permitAll() .pathMatchers("/ws/**").permitAll()
.anyExchange().authenticated()) .pathMatchers("/actuator/**").permitAll();
.build();
if (isDevOrTest) {
exchanges.pathMatchers("/swagger-ui.html").permitAll()
.pathMatchers("/swagger-ui/**").permitAll()
.pathMatchers("/api-docs/**").permitAll()
.pathMatchers("/v3/api-docs/**").permitAll()
.pathMatchers("/swagger-resources/**").permitAll()
.pathMatchers("/webjars/**").permitAll();
logger.info("SecurityConfig: Swagger路径已放行");
}
exchanges.anyExchange().authenticated();
return http.build();
} }
} }
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -16,19 +17,22 @@ class SecurityConfigTest {
@Mock @Mock
private JwtAuthenticationFilter jwtAuthenticationFilter; private JwtAuthenticationFilter jwtAuthenticationFilter;
@Mock
private Environment environment;
@Mock
private PasswordEncoder passwordEncoder;
private SecurityConfig securityConfig; private SecurityConfig securityConfig;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
securityConfig = new SecurityConfig(jwtAuthenticationFilter); securityConfig = new SecurityConfig(jwtAuthenticationFilter, environment);
} }
@Test @Test
void testPasswordEncoder() { void testPasswordEncoder() {
PasswordEncoder passwordEncoder = securityConfig.passwordEncoder();
assertThat(passwordEncoder).isNotNull(); assertThat(passwordEncoder).isNotNull();
assertThat(passwordEncoder).isInstanceOf(org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.class);
String rawPassword = "testPassword123"; String rawPassword = "testPassword123";
String encodedPassword = passwordEncoder.encode(rawPassword); String encodedPassword = passwordEncoder.encode(rawPassword);
@@ -41,8 +45,6 @@ class SecurityConfigTest {
@Test @Test
void testPasswordEncoder_SamePasswordDifferentHashes() { void testPasswordEncoder_SamePasswordDifferentHashes() {
PasswordEncoder passwordEncoder = securityConfig.passwordEncoder();
String rawPassword = "testPassword123"; String rawPassword = "testPassword123";
String hash1 = passwordEncoder.encode(rawPassword); String hash1 = passwordEncoder.encode(rawPassword);
String hash2 = passwordEncoder.encode(rawPassword); String hash2 = passwordEncoder.encode(rawPassword);
@@ -54,8 +56,6 @@ class SecurityConfigTest {
@Test @Test
void testPasswordEncoder_EmptyPassword() { void testPasswordEncoder_EmptyPassword() {
PasswordEncoder passwordEncoder = securityConfig.passwordEncoder();
String encodedPassword = passwordEncoder.encode(""); String encodedPassword = passwordEncoder.encode("");
assertThat(encodedPassword).isNotNull(); assertThat(encodedPassword).isNotNull();
@@ -64,8 +64,6 @@ class SecurityConfigTest {
@Test @Test
void testPasswordEncoder_Strength() { void testPasswordEncoder_Strength() {
PasswordEncoder passwordEncoder = securityConfig.passwordEncoder();
String rawPassword = "testPassword123"; String rawPassword = "testPassword123";
String encodedPassword = passwordEncoder.encode(rawPassword); String encodedPassword = passwordEncoder.encode(rawPassword);