From 6e56dcb6a229f4d02fe7325f75d5aecbc0e4d0d0 Mon Sep 17 00:00:00 2001 From: lzh Date: Wed, 11 Mar 2026 17:35:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(framework):=20API=20=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E3=80=81=E5=AE=89=E5=85=A8=E7=99=BD=E5=90=8D=E5=8D=95=E4=B8=8E?= =?UTF-8?q?=20Web=20=E9=85=8D=E7=BD=AE=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ApiSignatureProperties 配置类 - 调整签名自动配置与 Redis DAO 实现 - 更新安全白名单与 Web 属性配置 - 网关新增安保模块路由配置 Co-Authored-By: Claude Opus 4.6 --- .../config/ApiSignatureProperties.java | 32 ++ .../ViewshApiSignatureAutoConfiguration.java | 62 ++-- .../core/redis/ApiSignatureRedisDAO.java | 135 ++++--- .../config/AuthorizeRequestsCustomizer.java | 74 ++-- .../config/ViewshWebAutoConfiguration.java | 343 +++++++++--------- .../framework/web/config/WebProperties.java | 134 +++---- .../src/main/resources/application.yaml | 4 + 7 files changed, 427 insertions(+), 357 deletions(-) create mode 100644 viewsh-framework/viewsh-spring-boot-starter-protection/src/main/java/com/viewsh/framework/signature/config/ApiSignatureProperties.java 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 签名配置属性 + *

+ * 支持在 application.yaml 中配置 appId/appSecret,应用启动时自动加载到 Redis。 + * + *

+ * viewsh:
+ *   signature:
+ *     apps:
+ *       alarm-system: "your-app-secret"
+ *       third-party:  "another-secret"
+ * 
+ * + * @author lzh + */ +@ConfigurationProperties(prefix = "viewsh.signature") +@Data +public class ApiSignatureProperties { + + /** + * 签名应用列表:appId → appSecret + */ + private Map 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 前缀