diff --git a/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/config/ApiSignatureProperties.java b/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/config/ApiSignatureProperties.java
new file mode 100644
index 0000000..4cd1895
--- /dev/null
+++ b/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/config/ApiSignatureProperties.java
@@ -0,0 +1,32 @@
+package com.viewsh.framework.signature.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Map;
+
+/**
+ * API 签名配置属性
+ *
apps;
+
+}
diff --git a/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/config/ViewshApiSignatureAutoConfiguration.java b/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/config/ViewshApiSignatureAutoConfiguration.java
index 9aa434c..9f1d066 100644
--- a/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/config/ViewshApiSignatureAutoConfiguration.java
+++ b/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/config/ViewshApiSignatureAutoConfiguration.java
@@ -1,28 +1,34 @@
-package com.viewsh.framework.signature.config;
-
-import com.viewsh.framework.redis.config.ViewshRedisAutoConfiguration;
-import com.viewsh.framework.signature.core.aop.ApiSignatureAspect;
-import com.viewsh.framework.signature.core.redis.ApiSignatureRedisDAO;
-import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.context.annotation.Bean;
-import org.springframework.data.redis.core.StringRedisTemplate;
-
-/**
- * HTTP API 签名的自动配置类
- *
- * @author Zhougang
- */
-@AutoConfiguration(after = ViewshRedisAutoConfiguration.class)
-public class ViewshApiSignatureAutoConfiguration {
-
- @Bean
- public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
- return new ApiSignatureAspect(signatureRedisDAO);
- }
-
- @Bean
- public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {
- return new ApiSignatureRedisDAO(stringRedisTemplate);
- }
-
-}
+package com.viewsh.framework.signature.config;
+
+import com.viewsh.framework.redis.config.ViewshRedisAutoConfiguration;
+import com.viewsh.framework.signature.core.aop.ApiSignatureAspect;
+import com.viewsh.framework.signature.core.redis.ApiSignatureRedisDAO;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * HTTP API 签名的自动配置类
+ *
+ * @author Zhougang
+ */
+@AutoConfiguration(after = ViewshRedisAutoConfiguration.class)
+@EnableConfigurationProperties(ApiSignatureProperties.class)
+public class ViewshApiSignatureAutoConfiguration {
+
+ @Bean
+ public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
+ return new ApiSignatureAspect(signatureRedisDAO);
+ }
+
+ @Bean
+ public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate,
+ ApiSignatureProperties properties) {
+ ApiSignatureRedisDAO dao = new ApiSignatureRedisDAO(stringRedisTemplate);
+ // 启动时将配置文件中的 appId/appSecret 同步到 Redis
+ dao.initApps(properties.getApps());
+ return dao;
+ }
+
+}
diff --git a/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/core/redis/ApiSignatureRedisDAO.java b/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/core/redis/ApiSignatureRedisDAO.java
index f228d7d..8450d50 100644
--- a/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/core/redis/ApiSignatureRedisDAO.java
+++ b/viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/core/redis/ApiSignatureRedisDAO.java
@@ -1,57 +1,78 @@
-package com.viewsh.framework.signature.core.redis;
-
-import lombok.AllArgsConstructor;
-import org.springframework.data.redis.core.StringRedisTemplate;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * HTTP API 签名 Redis DAO
- *
- * @author Zhougang
- */
-@AllArgsConstructor
-public class ApiSignatureRedisDAO {
-
- private final StringRedisTemplate stringRedisTemplate;
-
- /**
- * 验签随机数
- *
- * KEY 格式:signature_nonce:%s // 参数为 随机数
- * VALUE 格式:String
- * 过期时间:不固定
- */
- private static final String SIGNATURE_NONCE = "api_signature_nonce:%s:%s";
-
- /**
- * 签名密钥
- *
- * HASH 结构
- * KEY 格式:%s // 参数为 appid
- * VALUE 格式:String
- * 过期时间:永不过期(预加载到 Redis)
- */
- private static final String SIGNATURE_APPID = "api_signature_app";
-
- // ========== 验签随机数 ==========
-
- public String getNonce(String appId, String nonce) {
- return stringRedisTemplate.opsForValue().get(formatNonceKey(appId, nonce));
- }
-
- public Boolean setNonce(String appId, String nonce, int time, TimeUnit timeUnit) {
- return stringRedisTemplate.opsForValue().setIfAbsent(formatNonceKey(appId, nonce), "", time, timeUnit);
- }
-
- private static String formatNonceKey(String appId, String nonce) {
- return String.format(SIGNATURE_NONCE, appId, nonce);
- }
-
- // ========== 签名密钥 ==========
-
- public String getAppSecret(String appId) {
- return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId);
- }
-
-}
+package com.viewsh.framework.signature.core.redis;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * HTTP API 签名 Redis DAO
+ *
+ * @author Zhougang
+ */
+@AllArgsConstructor
+@Slf4j
+public class ApiSignatureRedisDAO {
+
+ private final StringRedisTemplate stringRedisTemplate;
+
+ /**
+ * 验签随机数
+ *
+ * KEY 格式:signature_nonce:%s // 参数为 随机数
+ * VALUE 格式:String
+ * 过期时间:不固定
+ */
+ private static final String SIGNATURE_NONCE = "api_signature_nonce:%s:%s";
+
+ /**
+ * 签名密钥
+ *
+ * HASH 结构
+ * KEY 格式:%s // 参数为 appid
+ * VALUE 格式:String
+ * 过期时间:永不过期(预加载到 Redis)
+ */
+ private static final String SIGNATURE_APPID = "api_signature_app";
+
+ // ========== 验签随机数 ==========
+
+ public String getNonce(String appId, String nonce) {
+ return stringRedisTemplate.opsForValue().get(formatNonceKey(appId, nonce));
+ }
+
+ public Boolean setNonce(String appId, String nonce, int time, TimeUnit timeUnit) {
+ return stringRedisTemplate.opsForValue().setIfAbsent(formatNonceKey(appId, nonce), "", time, timeUnit);
+ }
+
+ private static String formatNonceKey(String appId, String nonce) {
+ return String.format(SIGNATURE_NONCE, appId, nonce);
+ }
+
+ // ========== 签名密钥 ==========
+
+ public String getAppSecret(String appId) {
+ return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId);
+ }
+
+ /**
+ * 从配置文件加载 appId/appSecret 到 Redis
+ *
+ * 先删除整个 Hash Key 再写入,确保 YAML 中移除的应用不会残留在 Redis 中。
+ *
+ * @param apps appId → appSecret 映射
+ */
+ public void initApps(Map apps) {
+ if (apps == null || apps.isEmpty()) {
+ stringRedisTemplate.delete(SIGNATURE_APPID);
+ log.info("[initApps][配置为空,已清除 Redis 中的签名应用]");
+ return;
+ }
+ stringRedisTemplate.delete(SIGNATURE_APPID);
+ stringRedisTemplate.opsForHash().putAll(SIGNATURE_APPID, apps);
+ log.info("[initApps][从配置文件加载 {} 个签名应用到 Redis: {}]", apps.size(), apps.keySet());
+ }
+
+}
diff --git a/viewsh-framework/viewsh-spring-boot-starter-security/src/main/java/com/viewsh/framework/security/config/AuthorizeRequestsCustomizer.java b/viewsh-framework/viewsh-spring-boot-starter-security/src/main/java/com/viewsh/framework/security/config/AuthorizeRequestsCustomizer.java
index af00f3e..e21b328 100644
--- a/viewsh-framework/viewsh-spring-boot-starter-security/src/main/java/com/viewsh/framework/security/config/AuthorizeRequestsCustomizer.java
+++ b/viewsh-framework/viewsh-spring-boot-starter-security/src/main/java/com/viewsh/framework/security/config/AuthorizeRequestsCustomizer.java
@@ -1,35 +1,39 @@
-package com.viewsh.framework.security.config;
-
-import com.viewsh.framework.web.config.WebProperties;
-import jakarta.annotation.Resource;
-import org.springframework.core.Ordered;
-import org.springframework.security.config.Customizer;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
-
-/**
- * 自定义的 URL 的安全配置
- * 目的:每个 Maven Module 可以自定义规则!
- *
- * @author 芋道源码
- */
-public abstract class AuthorizeRequestsCustomizer
- implements Customizer.AuthorizationManagerRequestMatcherRegistry>, Ordered {
-
- @Resource
- private WebProperties webProperties;
-
- protected String buildAdminApi(String url) {
- return webProperties.getAdminApi().getPrefix() + url;
- }
-
- protected String buildAppApi(String url) {
- return webProperties.getAppApi().getPrefix() + url;
- }
-
- @Override
- public int getOrder() {
- return 0;
- }
-
-}
+package com.viewsh.framework.security.config;
+
+import com.viewsh.framework.web.config.WebProperties;
+import jakarta.annotation.Resource;
+import org.springframework.core.Ordered;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
+
+/**
+ * 自定义的 URL 的安全配置
+ * 目的:每个 Maven Module 可以自定义规则!
+ *
+ * @author 芋道源码
+ */
+public abstract class AuthorizeRequestsCustomizer
+ implements Customizer.AuthorizationManagerRequestMatcherRegistry>, Ordered {
+
+ @Resource
+ private WebProperties webProperties;
+
+ protected String buildAdminApi(String url) {
+ return webProperties.getAdminApi().getPrefix() + url;
+ }
+
+ protected String buildAppApi(String url) {
+ return webProperties.getAppApi().getPrefix() + url;
+ }
+
+ protected String buildOpenApi(String url) {
+ return webProperties.getOpenApi().getPrefix() + url;
+ }
+
+ @Override
+ public int getOrder() {
+ return 0;
+ }
+
+}
diff --git a/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/web/config/ViewshWebAutoConfiguration.java b/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/web/config/ViewshWebAutoConfiguration.java
index 569ae64..4ccbf08 100644
--- a/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/web/config/ViewshWebAutoConfiguration.java
+++ b/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/web/config/ViewshWebAutoConfiguration.java
@@ -1,171 +1,172 @@
-package com.viewsh.framework.web.config;
-
-import cn.hutool.core.util.StrUtil;
-import com.viewsh.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
-import com.viewsh.framework.common.enums.WebFilterOrderEnum;
-import com.viewsh.framework.web.core.filter.CacheRequestBodyFilter;
-import com.viewsh.framework.web.core.filter.DemoFilter;
-import com.viewsh.framework.web.core.handler.GlobalExceptionHandler;
-import com.viewsh.framework.web.core.handler.GlobalResponseBodyHandler;
-import com.viewsh.framework.web.core.util.WebFrameworkUtils;
-import com.google.common.collect.Maps;
-import jakarta.servlet.Filter;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
-import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.boot.web.client.RestTemplateBuilder;
-import org.springframework.boot.web.servlet.FilterRegistrationBean;
-import org.springframework.cloud.client.loadbalancer.LoadBalanced;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Primary;
-import org.springframework.core.annotation.Order;
-import org.springframework.util.AntPathMatcher;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.cors.CorsConfiguration;
-import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-import org.springframework.web.filter.CorsFilter;
-import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
-
-import java.util.Map;
-import java.util.function.Predicate;
-
-@AutoConfiguration(beforeName = {
- "com.fhs.trans.config.TransServiceConfig" // cloud 独有:避免一键改包后,RestTemplate 初始化的冲突。可见 https://t.zsxq.com/T4yj7 帖子
-})
-@EnableConfigurationProperties(WebProperties.class)
-public class ViewshWebAutoConfiguration {
-
- /**
- * 应用名
- */
- @Value("${spring.application.name}")
- private String applicationName;
-
- @Bean
- public WebMvcRegistrations webMvcRegistrations(WebProperties webProperties) {
- return new WebMvcRegistrations() {
-
- @Override
- public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
- RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
- // 实例化时就带上前缀
- mapping.setPathPrefixes(buildPathPrefixes(webProperties));
- return mapping;
- }
-
- /**
- * 构建 prefix → 匹配条件的映射
- */
- private Map>> buildPathPrefixes(WebProperties webProperties) {
- AntPathMatcher antPathMatcher = new AntPathMatcher(".");
- Map>> pathPrefixes = Maps.newLinkedHashMapWithExpectedSize(2);
- putPathPrefix(pathPrefixes, webProperties.getAdminApi(), antPathMatcher);
- putPathPrefix(pathPrefixes, webProperties.getAppApi(), antPathMatcher);
- return pathPrefixes;
- }
-
- /**
- * 设置 API 前缀,仅仅匹配 controller 包下的
- */
- private void putPathPrefix(Map>> pathPrefixes, WebProperties.Api api, AntPathMatcher matcher) {
- if (api == null || StrUtil.isEmpty(api.getPrefix())) {
- return;
- }
- pathPrefixes.put(api.getPrefix(), // api 前缀
- clazz -> clazz.isAnnotationPresent(RestController.class)
- && matcher.match(api.getController(), clazz.getPackage().getName()));
- }
-
- };
- }
-
- @Bean
- @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
- public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogCommonApi apiErrorLogApi) {
- return new GlobalExceptionHandler(applicationName, apiErrorLogApi);
- }
-
- @Bean
- public GlobalResponseBodyHandler globalResponseBodyHandler() {
- return new GlobalResponseBodyHandler();
- }
-
- @Bean
- @SuppressWarnings("InstantiationOfUtilityClass")
- public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) {
- // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean
- return new WebFrameworkUtils(webProperties);
- }
-
- // ========== Filter 相关 ==========
-
- /**
- * 创建 CorsFilter Bean,解决跨域问题
- */
- @Bean
- @Order(value = WebFilterOrderEnum.CORS_FILTER) // 特殊:修复因执行顺序影响到跨域配置不生效问题
- public FilterRegistrationBean corsFilterBean() {
- // 创建 CorsConfiguration 对象
- CorsConfiguration config = new CorsConfiguration();
- config.setAllowCredentials(true);
- config.addAllowedOriginPattern("*"); // 设置访问源地址
- config.addAllowedHeader("*"); // 设置访问源请求头
- config.addAllowedMethod("*"); // 设置访问源请求方法
- // 创建 UrlBasedCorsConfigurationSource 对象
- UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
- source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
- return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER);
- }
-
- /**
- * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
- */
- @Bean
- public FilterRegistrationBean requestBodyCacheFilter() {
- return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
- }
-
- /**
- * 创建 DemoFilter Bean,演示模式
- */
- @Bean
- @ConditionalOnProperty(value = "viewsh.demo", havingValue = "true")
- public FilterRegistrationBean demoFilter() {
- return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
- }
-
- public static FilterRegistrationBean createFilterBean(T filter, Integer order) {
- FilterRegistrationBean bean = new FilterRegistrationBean<>(filter);
- bean.setOrder(order);
- return bean;
- }
-
- /**
- * 创建 RestTemplate 实例
- *
- * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
- */
- @Bean
- @ConditionalOnMissingBean
- @Primary
- public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
- return restTemplateBuilder.build();
- }
-
- /**
- * 创建 RestTemplate 实例(支持负载均衡)
- *
- * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
- */
- @Bean
- @LoadBalanced
- public RestTemplate loadBalancedRestTemplate(RestTemplateBuilder restTemplateBuilder) {
- return restTemplateBuilder.build();
- }
-
-}
+package com.viewsh.framework.web.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.viewsh.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
+import com.viewsh.framework.common.enums.WebFilterOrderEnum;
+import com.viewsh.framework.web.core.filter.CacheRequestBodyFilter;
+import com.viewsh.framework.web.core.filter.DemoFilter;
+import com.viewsh.framework.web.core.handler.GlobalExceptionHandler;
+import com.viewsh.framework.web.core.handler.GlobalResponseBodyHandler;
+import com.viewsh.framework.web.core.util.WebFrameworkUtils;
+import com.google.common.collect.Maps;
+import jakarta.servlet.Filter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.core.annotation.Order;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.util.Map;
+import java.util.function.Predicate;
+
+@AutoConfiguration(beforeName = {
+ "com.fhs.trans.config.TransServiceConfig" // cloud 独有:避免一键改包后,RestTemplate 初始化的冲突。可见 https://t.zsxq.com/T4yj7 帖子
+})
+@EnableConfigurationProperties(WebProperties.class)
+public class ViewshWebAutoConfiguration {
+
+ /**
+ * 应用名
+ */
+ @Value("${spring.application.name}")
+ private String applicationName;
+
+ @Bean
+ public WebMvcRegistrations webMvcRegistrations(WebProperties webProperties) {
+ return new WebMvcRegistrations() {
+
+ @Override
+ public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
+ RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
+ // 实例化时就带上前缀
+ mapping.setPathPrefixes(buildPathPrefixes(webProperties));
+ return mapping;
+ }
+
+ /**
+ * 构建 prefix → 匹配条件的映射
+ */
+ private Map>> buildPathPrefixes(WebProperties webProperties) {
+ AntPathMatcher antPathMatcher = new AntPathMatcher(".");
+ Map>> pathPrefixes = Maps.newLinkedHashMapWithExpectedSize(3);
+ putPathPrefix(pathPrefixes, webProperties.getAdminApi(), antPathMatcher);
+ putPathPrefix(pathPrefixes, webProperties.getAppApi(), antPathMatcher);
+ putPathPrefix(pathPrefixes, webProperties.getOpenApi(), antPathMatcher);
+ return pathPrefixes;
+ }
+
+ /**
+ * 设置 API 前缀,仅仅匹配 controller 包下的
+ */
+ private void putPathPrefix(Map>> pathPrefixes, WebProperties.Api api, AntPathMatcher matcher) {
+ if (api == null || StrUtil.isEmpty(api.getPrefix())) {
+ return;
+ }
+ pathPrefixes.put(api.getPrefix(), // api 前缀
+ clazz -> clazz.isAnnotationPresent(RestController.class)
+ && matcher.match(api.getController(), clazz.getPackage().getName()));
+ }
+
+ };
+ }
+
+ @Bean
+ @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+ public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogCommonApi apiErrorLogApi) {
+ return new GlobalExceptionHandler(applicationName, apiErrorLogApi);
+ }
+
+ @Bean
+ public GlobalResponseBodyHandler globalResponseBodyHandler() {
+ return new GlobalResponseBodyHandler();
+ }
+
+ @Bean
+ @SuppressWarnings("InstantiationOfUtilityClass")
+ public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) {
+ // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean
+ return new WebFrameworkUtils(webProperties);
+ }
+
+ // ========== Filter 相关 ==========
+
+ /**
+ * 创建 CorsFilter Bean,解决跨域问题
+ */
+ @Bean
+ @Order(value = WebFilterOrderEnum.CORS_FILTER) // 特殊:修复因执行顺序影响到跨域配置不生效问题
+ public FilterRegistrationBean corsFilterBean() {
+ // 创建 CorsConfiguration 对象
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ config.addAllowedOriginPattern("*"); // 设置访问源地址
+ config.addAllowedHeader("*"); // 设置访问源请求头
+ config.addAllowedMethod("*"); // 设置访问源请求方法
+ // 创建 UrlBasedCorsConfigurationSource 对象
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
+ return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER);
+ }
+
+ /**
+ * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
+ */
+ @Bean
+ public FilterRegistrationBean requestBodyCacheFilter() {
+ return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
+ }
+
+ /**
+ * 创建 DemoFilter Bean,演示模式
+ */
+ @Bean
+ @ConditionalOnProperty(value = "viewsh.demo", havingValue = "true")
+ public FilterRegistrationBean demoFilter() {
+ return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
+ }
+
+ public static FilterRegistrationBean createFilterBean(T filter, Integer order) {
+ FilterRegistrationBean bean = new FilterRegistrationBean<>(filter);
+ bean.setOrder(order);
+ return bean;
+ }
+
+ /**
+ * 创建 RestTemplate 实例
+ *
+ * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @Primary
+ public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
+ return restTemplateBuilder.build();
+ }
+
+ /**
+ * 创建 RestTemplate 实例(支持负载均衡)
+ *
+ * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder}
+ */
+ @Bean
+ @LoadBalanced
+ public RestTemplate loadBalancedRestTemplate(RestTemplateBuilder restTemplateBuilder) {
+ return restTemplateBuilder.build();
+ }
+
+}
diff --git a/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/web/config/WebProperties.java b/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/web/config/WebProperties.java
index 5ac860b..b8940f4 100644
--- a/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/web/config/WebProperties.java
+++ b/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/web/config/WebProperties.java
@@ -1,66 +1,68 @@
-package com.viewsh.framework.web.config;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
-
-import jakarta.validation.Valid;
-import jakarta.validation.constraints.NotEmpty;
-import jakarta.validation.constraints.NotNull;
-
-@ConfigurationProperties(prefix = "viewsh.web")
-@Validated
-@Data
-public class WebProperties {
-
- @NotNull(message = "APP API 不能为空")
- private Api appApi = new Api("/app-api", "**.controller.app.**");
- @NotNull(message = "Admin API 不能为空")
- private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
-
- @NotNull(message = "Admin UI 不能为空")
- private Ui adminUi;
-
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- @Valid
- public static class Api {
-
- /**
- * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
- *
- *
- * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
- * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
- *
- * @see ViewshWebAutoConfiguration#configurePathMatch(PathMatchConfigurer)
- */
- @NotEmpty(message = "API 前缀不能为空")
- private String prefix;
-
- /**
- * Controller 所在包的 Ant 路径规则
- *
- * 主要目的是,给该 Controller 设置指定的 {@link #prefix}
- */
- @NotEmpty(message = "Controller 所在包不能为空")
- private String controller;
-
- }
-
- @Data
- @Valid
- public static class Ui {
-
- /**
- * 访问地址
- */
- private String url;
-
- }
-
-}
+package com.viewsh.framework.web.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+
+@ConfigurationProperties(prefix = "viewsh.web")
+@Validated
+@Data
+public class WebProperties {
+
+ @NotNull(message = "APP API 不能为空")
+ private Api appApi = new Api("/app-api", "**.controller.app.**");
+ @NotNull(message = "Admin API 不能为空")
+ private Api adminApi = new Api("/admin-api", "**.controller.admin.**");
+
+ private Api openApi = new Api("/open-api", "**.controller.open.**");
+
+ @NotNull(message = "Admin UI 不能为空")
+ private Ui adminUi;
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Valid
+ public static class Api {
+
+ /**
+ * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
+ *
+ *
+ * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
+ * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
+ *
+ * @see ViewshWebAutoConfiguration#configurePathMatch(PathMatchConfigurer)
+ */
+ @NotEmpty(message = "API 前缀不能为空")
+ private String prefix;
+
+ /**
+ * Controller 所在包的 Ant 路径规则
+ *
+ * 主要目的是,给该 Controller 设置指定的 {@link #prefix}
+ */
+ @NotEmpty(message = "Controller 所在包不能为空")
+ private String controller;
+
+ }
+
+ @Data
+ @Valid
+ public static class Ui {
+
+ /**
+ * 访问地址
+ */
+ private String url;
+
+ }
+
+}
diff --git a/viewsh-gateway/src/main/resources/application.yaml b/viewsh-gateway/src/main/resources/application.yaml
index 7d85f76..a4941c7 100644
--- a/viewsh-gateway/src/main/resources/application.yaml
+++ b/viewsh-gateway/src/main/resources/application.yaml
@@ -208,6 +208,10 @@ spring:
- Path=/app-api/ops/**
filters:
- RewritePath=/app-api/ops/v3/api-docs, /v3/api-docs
+ - id: ops-open-api # 开放接口路由(签名验证,无需 Token)
+ uri: grayLb://ops-server
+ predicates:
+ - Path=/open-api/ops/**
x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀