From 78bc88b7a6ae5fd83be84927ce822f7d29228071 Mon Sep 17 00:00:00 2001 From: chenbowen Date: Fri, 17 Oct 2025 17:40:46 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=96=B0=E5=A2=9E=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=BC=96=E7=A0=81=E9=83=A8=E9=97=A8=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E5=85=B3=E8=81=94=E7=AE=A1=E7=90=86=202.=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=BB=9F=E4=B8=80=E7=9A=84=20api=20=E5=AF=B9=E5=A4=96?= =?UTF-8?q?=E9=97=A8=E6=88=B7=E7=AE=A1=E7=90=86=203.=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=90=84=E4=B8=AA=E6=A8=A1=E5=9D=97=E7=9A=84=20api=20=E5=91=BD?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- sql/dm/统一外部网关菜单_20251010.sql | 64 +-- sql/dm/统一对外网关_20251010.sql | 83 ++-- .../util/security/CryptoSignatureUtils.java | 217 ++++++++++ .../security/CryptoSignatureUtilsTest.java | 50 +++ .../ZtApiSignatureAutoConfiguration.java | 52 ++- .../signature/core/ApiSignatureVerifier.java | 111 +++++ .../core/aop/ApiSignatureAspect.java | 163 +------ .../core/config/ApiSignatureProperties.java | 68 +++ .../core/model/ApiSignatureRule.java | 50 +++ .../web/ApiSignatureHandlerInterceptor.java | 80 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../signature/core/ApiSignatureTest.java | 30 +- .../security/TokenAuthenticationFilter.java | 9 + .../src/main/resources/application.yaml | 4 +- .../src/main/resources/application.yaml | 4 +- .../zt-module-databus-server/pom.xml | 5 - .../ApiClientCredentialController.java | 79 ++++ .../admin/gateway/ApiGatewayController.java | 71 +-- .../gateway/ApiPolicyAuthController.java | 84 ---- .../convert/ApiClientCredentialConvert.java | 41 ++ .../gateway/convert/ApiPolicyAuthConvert.java | 25 -- .../ApiClientCredentialPageReqVO.java | 19 + .../credential/ApiClientCredentialRespVO.java | 42 ++ .../ApiClientCredentialSaveReqVO.java | 41 ++ .../ApiClientCredentialSimpleRespVO.java | 19 + .../definition/ApiDefinitionDetailRespVO.java | 12 - .../vo/definition/ApiDefinitionPageReqVO.java | 5 +- .../vo/definition/ApiDefinitionSaveReqVO.java | 13 - .../definition/ApiDefinitionStepRespVO.java | 3 - .../ApiDefinitionStepSaveReqVO.java | 3 - .../ApiDefinitionSummaryRespVO.java | 6 - .../gateway/ApiClientCredentialDO.java | 37 ++ .../dataobject/gateway/ApiDefinitionDO.java | 8 - .../dataobject/gateway/ApiPolicyAuthDO.java | 31 -- .../dal/dataobject/gateway/ApiStepDO.java | 2 - .../gateway/ApiClientCredentialMapper.java | 47 ++ .../mysql/gateway/ApiDefinitionMapper.java | 19 +- .../mysql/gateway/ApiPolicyAuthMapper.java | 36 -- .../enums/gateway/ApiStepTypeEnum.java | 3 +- .../config/ApiGatewayProperties.java | 68 ++- .../GatewayIntegrationConfiguration.java | 89 +--- .../gateway/core/ApiFlowAssembler.java | 72 +--- .../gateway/core/ApiFlowDispatcher.java | 3 +- .../gateway/core/ApiFlowRegistration.java | 1 + .../core/ApiGatewayErrorProcessor.java | 97 +++++ .../core/ApiGatewayExecutionService.java | 286 +++++++++++++ .../gateway/core/ApiGatewayRequestMapper.java | 252 ++++++++++- .../gateway/core/ErrorHandlingStrategy.java | 15 +- .../gateway/core/GatewayHeaderUtils.java | 81 ++++ .../gateway/core/IntegrationFlowManager.java | 8 +- .../gateway/core/MonitoringInterceptor.java | 4 +- .../gateway/core/PolicyAdvisorFactory.java | 118 +---- .../domain/ApiDefinitionAggregate.java | 3 - .../expression/GatewayExpressionHelper.java | 168 ++++++++ .../JsScriptExpressionEvaluator.java | 13 - .../expression/MvelExpressionEvaluator.java | 13 - .../expression/SpelExpressionEvaluator.java | 13 - .../gateway/init/GatewayPolicyMigration.java | 5 +- .../gateway/model/ApiGatewayResponse.java | 7 +- .../gateway/model/ApiInvocationContext.java | 10 +- .../gateway/policy/AuthPolicyEvaluator.java | 13 - .../policy/DefaultAuthPolicyEvaluator.java | 56 --- .../DefaultRateLimitPolicyEvaluator.java | 4 +- .../CachedBodyHttpServletRequest.java | 179 ++++++++ .../gateway/security/GatewayJwtResolver.java | 167 ++++++++ .../security/GatewaySecurityFilter.java | 405 +++++++++++++++++- .../SignatureValidationException.java | 18 + .../gateway/step/StepHandlerFactory.java | 15 +- .../gateway/step/impl/EndStepHandler.java | 74 ++++ .../gateway/step/impl/HttpStepHandler.java | 18 +- .../gateway/step/impl/StartStepHandler.java | 73 ++++ .../gateway/ApiClientCredentialService.java | 26 ++ .../service/gateway/ApiPolicyAuthService.java | 46 -- .../impl/ApiClientCredentialServiceImpl.java | 133 ++++++ .../impl/ApiDefinitionServiceImpl.java | 73 +++- .../impl/ApiPolicyAuthServiceImpl.java | 98 ----- .../GatewayServiceErrorCodeConstants.java | 21 +- .../src/main/resources/application.yml | 15 +- .../gateway/ApiGatewayControllerTest.java | 63 +++ .../core/ApiGatewayRequestMapperTest.java | 105 +++++ .../DefaultAuthPolicyEvaluatorTest.java | 56 +++ .../security/GatewaySecurityFilterTest.java | 159 +++++++ .../gateway/step/impl/EndStepHandlerTest.java | 100 +++++ .../step/impl/StartStepHandlerTest.java | 99 +++++ .../gateway/ApiDefinitionServiceImplTest.java | 74 ++-- .../src/test/resources/sql/clean.sql | 1 - .../src/test/resources/sql/create_tables.sql | 19 - .../src/main/resources/application.yml | 2 +- .../src/main/resources/application.yaml | 4 +- .../src/main/resources/application.yaml | 4 +- .../src/main/resources/application.yaml | 4 +- .../src/main/resources/application.yaml | 4 +- .../src/main/resources/application.yml | 4 +- .../controller/admin/dept/DeptController.java | 9 + .../admin/dept/vo/dept/DeptCodeInitReqVO.java | 0 .../admin/dept/vo/dept/DeptSaveReqVO.java | 2 +- .../controller/admin/sync/SyncController.java | 7 +- .../system/dal/mysql/dept/DeptMapper.java | 15 + .../system/service/dept/DeptService.java | 5 + .../system/service/dept/DeptServiceImpl.java | 155 ++++++- .../service/oauth2/EbanOAuth2Service.java | 4 + .../service/oauth2/EbanOAuth2ServiceImpl.java | 59 ++- .../system/util/sync/SyncVerifyUtil.java | 113 +---- .../service/dept/DeptServiceImplTest.java | 107 ++++- .../src/main/resources/application.yaml | 4 +- 106 files changed, 4200 insertions(+), 1377 deletions(-) create mode 100644 zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java create mode 100644 zt-framework/zt-common/src/test/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtilsTest.java create mode 100644 zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/ApiSignatureVerifier.java create mode 100644 zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/config/ApiSignatureProperties.java create mode 100644 zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/model/ApiSignatureRule.java create mode 100644 zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/web/ApiSignatureHandlerInterceptor.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiClientCredentialController.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiPolicyAuthController.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiClientCredentialConvert.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiPolicyAuthConvert.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialPageReqVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialRespVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSaveReqVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSimpleRespVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiClientCredentialDO.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiPolicyAuthDO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiClientCredentialMapper.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiPolicyAuthMapper.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessor.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/GatewayHeaderUtils.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/GatewayExpressionHelper.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/JsScriptExpressionEvaluator.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/MvelExpressionEvaluator.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/SpelExpressionEvaluator.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/AuthPolicyEvaluator.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultAuthPolicyEvaluator.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/CachedBodyHttpServletRequest.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewayJwtResolver.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/SignatureValidationException.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/EndStepHandler.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiClientCredentialService.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiPolicyAuthService.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiClientCredentialServiceImpl.java delete mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyAuthServiceImpl.java create mode 100644 zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java create mode 100644 zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapperTest.java create mode 100644 zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultAuthPolicyEvaluatorTest.java create mode 100644 zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java create mode 100644 zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/EndStepHandlerTest.java create mode 100644 zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandlerTest.java create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptCodeInitReqVO.java diff --git a/pom.xml b/pom.xml index aec4d8b8..ae6b1ece 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ zt-module-template - + zt-module-databus diff --git a/sql/dm/统一外部网关菜单_20251010.sql b/sql/dm/统一外部网关菜单_20251010.sql index e7c36fe3..832a673c 100644 --- a/sql/dm/统一外部网关菜单_20251010.sql +++ b/sql/dm/统一外部网关菜单_20251010.sql @@ -1,31 +1,37 @@ +-- 统一外部 API 网关菜单权限初始化(DM8) +-- 可重复执行的初始化脚本,统一外部网关所有页面改为权限菜单控制 +DELETE FROM system_menu + WHERE id IN (6500,6501,6502,6503, + 650101,650102,650103,650104,650105,650106, + 650201,650202,650203,650204, + 650301,650302,650303,650304); --- 清理旧数据,确保脚本可重复执行 -DELETE FROM system_menu WHERE id IN (6500,6501,650101,650102,650103); +INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, + status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) +VALUES + (6500, '数据总线', '', 1, 20, 1, 'databus', 'ep:data-board', '', 'DatabusRoot', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (6501, 'API 定义', 'databus:gateway:query', 2, 10, 6500, 'gateway', 'ep:list', 'databus/gateway/index', 'DatabusGateway', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (6502, '客户端凭证', 'databus:credential:query', 2, 20, 6500, 'credential', 'ep:key', 'databus/credential/index', 'DatabusCredential', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (6503, '限流策略', 'databus:policy:query', 2, 30, 6500, 'policy/rate-limit', 'ep:stopwatch', 'databus/policy/RateLimitPolicy', 'DatabusRateLimitPolicy', + 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'); --- 顶级目录(父级假定为 id=2 的系统管理目录) -INSERT INTO system_menu ( - id, name, permission, type, sort, parent_id, - path, icon, component, status, component_name -) VALUES ( - 6500, '统一外部网关', '', 1, 20, 1, - 'databus', 'ep:data-line', '', 0, NULL -); - --- API 门户页面 -INSERT INTO system_menu ( - id, name, permission, type, sort, parent_id, - path, icon, component, status, component_name -) VALUES ( - 6501, 'API 门户', 'databus:gateway:query', 2, 1, 6500, - 'databus-gateway', 'ep:cpu', 'databus/gateway/index', 0, 'DatabusGateway' -); - --- 页面内操作按钮权限 -INSERT INTO system_menu ( - id, name, permission, type, sort, parent_id, - path, icon, component, status -) VALUES - (650101, 'API 列表', 'databus:gateway:query', 3, 1, 6501, '', '', '', 0), - (650102, 'API 调试', 'databus:gateway:invoke', 3, 2, 6501, '', '', '', 0), - (650103, '刷新定义', 'databus:gateway:refresh', 3, 3, 6501, '', '', '', 0); -d \ No newline at end of file +INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, + status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) +VALUES + (650101, 'API 查询', 'databus:gateway:query', 3, 1, 6501, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650102, 'API 新建', 'databus:gateway:create', 3, 2, 6501, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650103, 'API 编辑', 'databus:gateway:update', 3, 3, 6501, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650104, 'API 删除', 'databus:gateway:delete', 3, 4, 6501, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650105, 'API 调试', 'databus:gateway:invoke', 3, 5, 6501, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650106, 'API 刷新', 'databus:gateway:refresh', 3, 6, 6501, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650201, '凭证查询', 'databus:credential:query', 3, 1, 6502, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650202, '凭证新增', 'databus:credential:create', 3, 2, 6502, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650203, '凭证修改', 'databus:credential:update', 3, 3, 6502, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650204, '凭证删除', 'databus:credential:delete', 3, 4, 6502, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650301, '策略查询', 'databus:policy:query', 3, 1, 6503, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650302, '策略新增', 'databus:policy:create', 3, 2, 6503, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650303, '策略修改', 'databus:policy:update', 3, 3, 6503, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'), + (650304, '策略删除', 'databus:policy:delete', 3, 4, 6503, '', '', '', '', 0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'); diff --git a/sql/dm/统一对外网关_20251010.sql b/sql/dm/统一对外网关_20251010.sql index b4c39768..55f7f82f 100644 --- a/sql/dm/统一对外网关_20251010.sql +++ b/sql/dm/统一对外网关_20251010.sql @@ -10,17 +10,13 @@ CREATE TABLE databus_api_definition ( id BIGINT NOT NULL PRIMARY KEY, tenant_id BIGINT NOT NULL, api_code VARCHAR(128) NOT NULL, - uri_pattern VARCHAR(256) NOT NULL, http_method VARCHAR(16) NOT NULL, version VARCHAR(32) NOT NULL, status SMALLINT DEFAULT 0 NOT NULL, description VARCHAR(512), - auth_policy_id BIGINT, rate_limit_id BIGINT, response_template CLOB, - cache_strategy VARCHAR(128), updated_at DATETIME, - grey_released BIT DEFAULT '0' NOT NULL, creator VARCHAR(64) DEFAULT '' NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, updater VARCHAR(64) DEFAULT '' NOT NULL, @@ -30,23 +26,18 @@ CREATE TABLE databus_api_definition ( CREATE UNIQUE INDEX uk_databus_api_definition_code_ver ON databus_api_definition (tenant_id, api_code, version); CREATE INDEX idx_databus_api_definition_status ON databus_api_definition (tenant_id, status); -CREATE INDEX idx_databus_api_definition_policy ON databus_api_definition (tenant_id, auth_policy_id, rate_limit_id); COMMENT ON TABLE databus_api_definition IS '统一外部 API 门户 - API 定义表'; COMMENT ON COLUMN databus_api_definition.id IS '主键 ID'; COMMENT ON COLUMN databus_api_definition.tenant_id IS '租户编号'; COMMENT ON COLUMN databus_api_definition.api_code IS 'API 编码'; -COMMENT ON COLUMN databus_api_definition.uri_pattern IS '匹配路径模板'; COMMENT ON COLUMN databus_api_definition.http_method IS 'HTTP 方法'; COMMENT ON COLUMN databus_api_definition.version IS '版本号'; COMMENT ON COLUMN databus_api_definition.status IS '发布状态'; COMMENT ON COLUMN databus_api_definition.description IS '描述信息'; -COMMENT ON COLUMN databus_api_definition.auth_policy_id IS '认证策略 ID'; COMMENT ON COLUMN databus_api_definition.rate_limit_id IS '限流策略 ID'; COMMENT ON COLUMN databus_api_definition.response_template IS '响应模板 JSON'; -COMMENT ON COLUMN databus_api_definition.cache_strategy IS '缓存策略配置'; COMMENT ON COLUMN databus_api_definition.updated_at IS '业务更新时间'; -COMMENT ON COLUMN databus_api_definition.grey_released IS '灰度发布标记'; COMMENT ON COLUMN databus_api_definition.creator IS '创建者'; COMMENT ON COLUMN databus_api_definition.create_time IS '创建时间'; COMMENT ON COLUMN databus_api_definition.updater IS '更新者'; @@ -89,38 +80,6 @@ COMMENT ON COLUMN databus_api_flow_publish.updater IS '更新者'; COMMENT ON COLUMN databus_api_flow_publish.update_time IS '更新时间'; COMMENT ON COLUMN databus_api_flow_publish.deleted IS '逻辑删除标记'; --- ---------------------------- --- Table structure for databus_policy_auth --- ---------------------------- -CREATE TABLE databus_policy_auth ( - id BIGINT NOT NULL PRIMARY KEY, - tenant_id BIGINT NOT NULL, - name VARCHAR(128) NOT NULL, - type VARCHAR(64) NOT NULL, - config CLOB, - description VARCHAR(512), - creator VARCHAR(64) DEFAULT '' NOT NULL, - create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - updater VARCHAR(64) DEFAULT '' NOT NULL, - update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - deleted BIT DEFAULT '0' NOT NULL -); - -CREATE UNIQUE INDEX uk_databus_policy_auth_name ON databus_policy_auth (tenant_id, name); - -COMMENT ON TABLE databus_policy_auth IS '统一外部 API 门户 - 认证策略表'; -COMMENT ON COLUMN databus_policy_auth.id IS '主键 ID'; -COMMENT ON COLUMN databus_policy_auth.tenant_id IS '租户编号'; -COMMENT ON COLUMN databus_policy_auth.name IS '策略名称'; -COMMENT ON COLUMN databus_policy_auth.type IS '策略类型'; -COMMENT ON COLUMN databus_policy_auth.config IS '策略配置 JSON'; -COMMENT ON COLUMN databus_policy_auth.description IS '描述信息'; -COMMENT ON COLUMN databus_policy_auth.creator IS '创建者'; -COMMENT ON COLUMN databus_policy_auth.create_time IS '创建时间'; -COMMENT ON COLUMN databus_policy_auth.updater IS '更新者'; -COMMENT ON COLUMN databus_policy_auth.update_time IS '更新时间'; -COMMENT ON COLUMN databus_policy_auth.deleted IS '逻辑删除标记'; - -- ---------------------------- -- Table structure for databus_policy_rate_limit -- ---------------------------- @@ -168,7 +127,6 @@ CREATE TABLE databus_api_step ( response_mapping_expr CLOB, transform_id BIGINT, timeout BIGINT, - retry_strategy CLOB, fallback_strategy CLOB, condition_expr CLOB, stop_on_error BIT DEFAULT '0' NOT NULL, @@ -193,7 +151,6 @@ COMMENT ON COLUMN databus_api_step.request_mapping_expr IS '请求映射表达 COMMENT ON COLUMN databus_api_step.response_mapping_expr IS '响应映射表达式'; COMMENT ON COLUMN databus_api_step.transform_id IS '默认变换 ID'; COMMENT ON COLUMN databus_api_step.timeout IS '超时时间(毫秒)'; -COMMENT ON COLUMN databus_api_step.retry_strategy IS '重试策略 JSON'; COMMENT ON COLUMN databus_api_step.fallback_strategy IS '降级策略 JSON'; COMMENT ON COLUMN databus_api_step.condition_expr IS '执行条件表达式'; COMMENT ON COLUMN databus_api_step.stop_on_error IS '出错是否终止'; @@ -239,3 +196,43 @@ COMMENT ON COLUMN databus_api_transform.create_time IS '创建时间'; COMMENT ON COLUMN databus_api_transform.updater IS '更新者'; COMMENT ON COLUMN databus_api_transform.update_time IS '更新时间'; COMMENT ON COLUMN databus_api_transform.deleted IS '逻辑删除标记'; + + + -- 统一外部网关 - 客户端凭证表(DM8) +-- 可重复执行的建表脚本,执行前请备份历史数据 + +DROP TABLE IF EXISTS databus_api_client_credential; + +CREATE TABLE databus_api_client_credential ( + id BIGINT NOT NULL PRIMARY KEY, + app_id VARCHAR(64) NOT NULL, + app_name VARCHAR(128), + encryption_key VARCHAR(512) NOT NULL, + encryption_type VARCHAR(32) NOT NULL, + signature_type VARCHAR(32) NOT NULL, + enabled BIT DEFAULT '1' NOT NULL, + remark VARCHAR(255), + creator VARCHAR(64) DEFAULT '' NOT NULL, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater VARCHAR(64) DEFAULT '' NOT NULL, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted BIT DEFAULT '0' NOT NULL +); + +CREATE UNIQUE INDEX uk_databus_api_client_credential_app ON databus_api_client_credential (app_id); +CREATE INDEX idx_databus_api_client_credential_enabled ON databus_api_client_credential (enabled); + +COMMENT ON TABLE databus_api_client_credential IS '统一外部 API 门户 - 客户端凭证表'; +COMMENT ON COLUMN databus_api_client_credential.id IS '主键 ID'; +COMMENT ON COLUMN databus_api_client_credential.app_id IS '客户端标识'; +COMMENT ON COLUMN databus_api_client_credential.app_name IS '客户端名称'; +COMMENT ON COLUMN databus_api_client_credential.encryption_key IS '加密密钥 Base64'; +COMMENT ON COLUMN databus_api_client_credential.encryption_type IS '加密算法'; +COMMENT ON COLUMN databus_api_client_credential.signature_type IS '签名算法'; +COMMENT ON COLUMN databus_api_client_credential.enabled IS '是否启用'; +COMMENT ON COLUMN databus_api_client_credential.remark IS '备注'; +COMMENT ON COLUMN databus_api_client_credential.creator IS '创建者'; +COMMENT ON COLUMN databus_api_client_credential.create_time IS '创建时间'; +COMMENT ON COLUMN databus_api_client_credential.updater IS '更新者'; +COMMENT ON COLUMN databus_api_client_credential.update_time IS '更新时间'; +COMMENT ON COLUMN databus_api_client_credential.deleted IS '逻辑删除标记'; diff --git a/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java new file mode 100644 index 00000000..79e405d2 --- /dev/null +++ b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java @@ -0,0 +1,217 @@ +package com.zt.plat.framework.common.util.security; + +import cn.hutool.crypto.SecureUtil; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * 通用的签名、加解密工具类 + */ +public final class CryptoSignatureUtils { + + public static final String ENCRYPT_TYPE_AES = "AES"; + public static final String ENCRYPT_TYPE_DES = "DES"; + public static final String SIGNATURE_TYPE_MD5 = "MD5"; + public static final String SIGNATURE_TYPE_SHA256 = "SHA256"; + + private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding"; + private static final String SIGNATURE_FIELD = "signature"; + + private CryptoSignatureUtils() { + } + + /** + * 生成 AES 密钥(SecretKeySpec) + * + * @param password 密钥字符串 + * @return SecretKeySpec + */ + public static SecretKeySpec getSecretKey(String password) { + try { + KeyGenerator kg = KeyGenerator.getInstance("AES"); + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + random.setSeed(password.getBytes(StandardCharsets.UTF_8)); + kg.init(128, random); + SecretKey secretKey = kg.generateKey(); + return new SecretKeySpec(secretKey.getEncoded(), "AES"); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Failed to generate AES secret key", ex); + } + } + + /** + * 对称加密(Base64 格式输出) + * + * @param plaintext 明文内容 + * @param key 密钥 + * @param type 加密类型,支持 AES、DES + * @return 密文(Base64 格式) + */ + public static String encrypt(String plaintext, String key, String type) { + if (ENCRYPT_TYPE_AES.equalsIgnoreCase(type)) { + try { + Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key)); + byte[] result = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(result); + } catch (Exception ex) { + throw new IllegalStateException("Failed to encrypt using AES", ex); + } + } else if (ENCRYPT_TYPE_DES.equalsIgnoreCase(type)) { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + byte[] desKey = new byte[8]; + System.arraycopy(keyBytes, 0, desKey, 0, Math.min(keyBytes.length, desKey.length)); + byte[] encrypted = SecureUtil.des(desKey).encrypt(plaintext.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encrypted); + } else { + throw new IllegalArgumentException("Unsupported encryption type: " + type); + } + } + + /** + * 对称解密(输入为 Base64 格式密文) + * + * @param ciphertext 密文内容(Base64 格式) + * @param key 密钥 + * @param type 加密类型,支持 AES、DES + * @return 明文内容 + */ + public static String decrypt(String ciphertext, String key, String type) { + if (ciphertext == null) { + return null; + } + if (ENCRYPT_TYPE_AES.equalsIgnoreCase(type)) { + try { + Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key)); + byte[] decoded = decodeBase64Ciphertext(ciphertext); + byte[] result = cipher.doFinal(decoded); + return new String(result, StandardCharsets.UTF_8); + } catch (Exception ex) { + throw new IllegalStateException("Failed to decrypt using AES", ex); + } + } else if (ENCRYPT_TYPE_DES.equalsIgnoreCase(type)) { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + byte[] desKey = new byte[8]; + System.arraycopy(keyBytes, 0, desKey, 0, Math.min(keyBytes.length, desKey.length)); + byte[] decoded = decodeBase64Ciphertext(ciphertext); + byte[] decrypted = SecureUtil.des(desKey).decrypt(decoded); + return new String(decrypted, StandardCharsets.UTF_8); + } else { + throw new IllegalArgumentException("Unsupported encryption type: " + type); + } + } + + /** + * 验证请求签名 + * + * @param reqMap 请求参数 Map + * @param type 签名算法类型,支持 MD5、SHA256 + * @return 签名是否有效 + */ + public static boolean verifySignature(Map reqMap, String type) { + Map sortedMap = new TreeMap<>(reqMap); + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : sortedMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (SIGNATURE_FIELD.equals(key) || value == null) { + continue; + } + sb.append(key).append('='); + sb.append(value); + sb.append('&'); + } + if (sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + String provided = (String) reqMap.get(SIGNATURE_FIELD); + if (provided == null) { + return false; + } + String computed; + if (SIGNATURE_TYPE_MD5.equalsIgnoreCase(type)) { + computed = SecureUtil.md5(sb.toString()); + } else if (SIGNATURE_TYPE_SHA256.equalsIgnoreCase(type)) { + computed = SecureUtil.sha256(sb.toString()); + } else { + throw new IllegalArgumentException("Unsupported signature type: " + type); + } + return provided.equalsIgnoreCase(computed); + } + + private static byte[] decodeBase64Ciphertext(String ciphertext) { + IllegalArgumentException last = null; + for (String candidate : buildBase64Candidates(ciphertext)) { + if (candidate == null || candidate.isEmpty()) { + continue; + } + try { + return Base64.getDecoder().decode(candidate); + } catch (IllegalArgumentException ex) { + last = ex; + } + } + throw last != null ? last : new IllegalArgumentException("Invalid Base64 content"); + } + + private static Set buildBase64Candidates(String ciphertext) { + Set candidates = new LinkedHashSet<>(); + if (ciphertext == null) { + return candidates; + } + String trimmed = ciphertext.trim(); + candidates.add(trimmed); + + String withoutWhitespace = stripWhitespace(trimmed); + candidates.add(withoutWhitespace); + + if (trimmed.indexOf(' ') >= 0) { + String restoredPlus = trimmed.replace(' ', '+'); + candidates.add(restoredPlus); + candidates.add(stripWhitespace(restoredPlus)); + } + + String urlNormalised = withoutWhitespace + .replace('-', '+') + .replace('_', '/'); + candidates.add(urlNormalised); + + return candidates; + } + + private static String stripWhitespace(String value) { + if (value == null) { + return null; + } + boolean hasWhitespace = false; + for (int i = 0; i < value.length(); i++) { + if (Character.isWhitespace(value.charAt(i))) { + hasWhitespace = true; + break; + } + } + if (!hasWhitespace) { + return value; + } + StringBuilder sb = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (!Character.isWhitespace(ch)) { + sb.append(ch); + } + } + return sb.toString(); + } +} diff --git a/zt-framework/zt-common/src/test/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtilsTest.java b/zt-framework/zt-common/src/test/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtilsTest.java new file mode 100644 index 00000000..be71f3e9 --- /dev/null +++ b/zt-framework/zt-common/src/test/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtilsTest.java @@ -0,0 +1,50 @@ +package com.zt.plat.framework.common.util.security; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CryptoSignatureUtilsTest { + + @Test + void decryptShouldIgnoreWhitespaceInCiphertext() { + String key = "test-key"; + String plaintext = "{\"sample\":123}"; + + String encrypted = CryptoSignatureUtils.encrypt(plaintext, key, CryptoSignatureUtils.ENCRYPT_TYPE_AES); + int splitIndex = Math.max(1, encrypted.length() / 2); + String cipherWithWhitespace = " " + encrypted.substring(0, splitIndex) + + " \n\t " + + encrypted.substring(splitIndex); + + String decrypted = CryptoSignatureUtils.decrypt(cipherWithWhitespace, key, CryptoSignatureUtils.ENCRYPT_TYPE_AES); + + assertEquals(plaintext, decrypted); + } + + @Test + void decryptShouldRestorePlusCharactersConvertedToSpaces() { + String key = "test-key"; + String basePlaintext = "payload-"; + + String encryptedWithPlus = null; + String chosenPlaintext = null; + for (int i = 0; i < 100; i++) { + String candidatePlaintext = basePlaintext + i; + String candidateEncrypted = CryptoSignatureUtils.encrypt(candidatePlaintext, key, CryptoSignatureUtils.ENCRYPT_TYPE_AES); + if (candidateEncrypted.indexOf('+') >= 0) { + encryptedWithPlus = candidateEncrypted; + chosenPlaintext = candidatePlaintext; + break; + } + } + + assertNotNull(encryptedWithPlus, "Expected to generate ciphertext containing '+' character"); + + String mutatedCipher = encryptedWithPlus.replace('+', ' '); + String decrypted = CryptoSignatureUtils.decrypt(mutatedCipher, key, CryptoSignatureUtils.ENCRYPT_TYPE_AES); + + assertEquals(chosenPlaintext, decrypted); + } +} diff --git a/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/config/ZtApiSignatureAutoConfiguration.java b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/config/ZtApiSignatureAutoConfiguration.java index f9c9ae97..5f5e7804 100644 --- a/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/config/ZtApiSignatureAutoConfiguration.java +++ b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/config/ZtApiSignatureAutoConfiguration.java @@ -1,11 +1,20 @@ package com.zt.plat.framework.signature.config; import com.zt.plat.framework.redis.config.ZtRedisAutoConfiguration; -import com.zt.plat.framework.signature.core.aop.ApiSignatureAspect; +import com.zt.plat.framework.signature.core.ApiSignatureVerifier; +import com.zt.plat.framework.signature.core.config.ApiSignatureProperties; import com.zt.plat.framework.signature.core.redis.ApiSignatureRedisDAO; +import com.zt.plat.framework.signature.core.web.ApiSignatureHandlerInterceptor; 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; +import org.springframework.util.CollectionUtils; +import org.springframework.web.servlet.config.annotation.InterceptorRegistration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; /** * HTTP API 签名的自动配置类 @@ -13,16 +22,47 @@ import org.springframework.data.redis.core.StringRedisTemplate; * @author Zhougang */ @AutoConfiguration(after = ZtRedisAutoConfiguration.class) +@EnableConfigurationProperties(ApiSignatureProperties.class) public class ZtApiSignatureAutoConfiguration { - @Bean - public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) { - return new ApiSignatureAspect(signatureRedisDAO); - } - @Bean public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) { return new ApiSignatureRedisDAO(stringRedisTemplate); } + @Bean + public ApiSignatureVerifier apiSignatureVerifier(ApiSignatureRedisDAO signatureRedisDAO) { + return new ApiSignatureVerifier(signatureRedisDAO); + } + + @Bean + public ApiSignatureHandlerInterceptor apiSignatureHandlerInterceptor(ApiSignatureVerifier verifier, + ApiSignatureProperties properties) { + return new ApiSignatureHandlerInterceptor(verifier, properties); + } + + @Bean + public WebMvcConfigurer apiSignatureWebMvcConfigurer(ApiSignatureHandlerInterceptor interceptor, + ApiSignatureProperties properties) { + return new WebMvcConfigurer() { + @Override + public void addInterceptors(InterceptorRegistry registry) { + if (!properties.isEnabled()) { + return; + } + InterceptorRegistration registration = registry.addInterceptor(interceptor); + List includePaths = properties.getIncludePaths(); + if (CollectionUtils.isEmpty(includePaths)) { + registration.addPathPatterns("/**"); + } else { + registration.addPathPatterns(includePaths.toArray(new String[0])); + } + List excludePaths = properties.getExcludePaths(); + if (!CollectionUtils.isEmpty(excludePaths)) { + registration.excludePathPatterns(excludePaths.toArray(new String[0])); + } + } + }; + } + } diff --git a/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/ApiSignatureVerifier.java b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/ApiSignatureVerifier.java new file mode 100644 index 00000000..640904e8 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/ApiSignatureVerifier.java @@ -0,0 +1,111 @@ +package com.zt.plat.framework.signature.core; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.zt.plat.framework.common.util.servlet.ServletUtils; +import com.zt.plat.framework.signature.core.model.ApiSignatureRule; +import com.zt.plat.framework.signature.core.redis.ApiSignatureRedisDAO; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import static com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; + +/** + * API 签名校验器 + */ +@Slf4j +@RequiredArgsConstructor +public class ApiSignatureVerifier { + + private final ApiSignatureRedisDAO signatureRedisDAO; + + public boolean verify(ApiSignatureRule rule, HttpServletRequest request) { + // 1. 校验请求头 + verifyHeaders(rule, request); + + // 2. 校验 appId 对应的 appSecret 是否存在 + String appId = request.getHeader(rule.getAppId()); + String appSecret = signatureRedisDAO.getAppSecret(appId); + Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId); + + // 3. 校验签名 + String clientSignature = request.getHeader(rule.getSign()); + String serverSignatureString = buildSignatureString(rule, request, appSecret); + String serverSignature = DigestUtil.sha256Hex(serverSignatureString); + if (ObjUtil.notEqual(clientSignature, serverSignature)) { + throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage()); + } + + // 4. 缓存 nonce,防止重复请求 + String nonce = request.getHeader(rule.getNonce()); + if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, rule.getTimeout() * 2, rule.getTimeUnit()))) { + String timestamp = request.getHeader(rule.getTimestamp()); + log.info("[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]", appId, timestamp, nonce, clientSignature); + throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), "存在重复请求"); + } + return true; + } + + private void verifyHeaders(ApiSignatureRule rule, HttpServletRequest request) { + String appId = request.getHeader(rule.getAppId()); + String timestamp = request.getHeader(rule.getTimestamp()); + String nonce = request.getHeader(rule.getNonce()); + String sign = request.getHeader(rule.getSign()); + if (StrUtil.isBlank(appId) || StrUtil.isBlank(timestamp) || StrUtil.isBlank(sign) || StrUtil.length(nonce) < 10) { + throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage()); + } + + long expireTime = rule.getTimeUnit().toMillis(rule.getTimeout()); + long requestTimestamp; + try { + requestTimestamp = Long.parseLong(timestamp); + } catch (NumberFormatException ex) { + throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage()); + } + long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); + if (timestampDisparity > expireTime) { + throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage()); + } + + if (signatureRedisDAO.getNonce(appId, nonce) != null) { + throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage()); + } + } + + private String buildSignatureString(ApiSignatureRule rule, HttpServletRequest request, String appSecret) { + SortedMap parameterMap = getRequestParameterMap(request); + SortedMap headerMap = getRequestHeaderMap(rule, request); + String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); + return MapUtil.join(parameterMap, "&", "=") + + requestBody + + MapUtil.join(headerMap, "&", "=") + + appSecret; + } + + private SortedMap getRequestHeaderMap(ApiSignatureRule rule, HttpServletRequest request) { + SortedMap sortedMap = new TreeMap<>(); + sortedMap.put(rule.getAppId(), request.getHeader(rule.getAppId())); + sortedMap.put(rule.getTimestamp(), request.getHeader(rule.getTimestamp())); + sortedMap.put(rule.getNonce(), request.getHeader(rule.getNonce())); + return sortedMap; + } + + private SortedMap getRequestParameterMap(HttpServletRequest request) { + SortedMap sortedMap = new TreeMap<>(); + for (Map.Entry entry : request.getParameterMap().entrySet()) { + sortedMap.put(entry.getKey(), entry.getValue()[0]); + } + return sortedMap; + } +} diff --git a/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/aop/ApiSignatureAspect.java b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/aop/ApiSignatureAspect.java index 6729848c..815e8a20 100644 --- a/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/aop/ApiSignatureAspect.java +++ b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/aop/ApiSignatureAspect.java @@ -1,29 +1,18 @@ package com.zt.plat.framework.signature.core.aop; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.BooleanUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.crypto.digest.DigestUtil; -import com.zt.plat.framework.common.exception.ServiceException; -import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants; import com.zt.plat.framework.common.util.servlet.ServletUtils; +import com.zt.plat.framework.signature.core.ApiSignatureVerifier; import com.zt.plat.framework.signature.core.annotation.ApiSignature; +import com.zt.plat.framework.signature.core.config.ApiSignatureProperties; +import com.zt.plat.framework.signature.core.model.ApiSignatureRule; import com.zt.plat.framework.signature.core.redis.ApiSignatureRedisDAO; import jakarta.servlet.http.HttpServletRequest; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; -import java.util.Map; import java.util.Objects; -import java.util.SortedMap; -import java.util.TreeMap; - -import static com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; /** * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名 @@ -32,143 +21,33 @@ import static com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConsta */ @Aspect @Slf4j -@AllArgsConstructor +@Deprecated public class ApiSignatureAspect { - private final ApiSignatureRedisDAO signatureRedisDAO; + private final ApiSignatureVerifier verifier; + private final ApiSignatureProperties properties; + + public ApiSignatureAspect(ApiSignatureRedisDAO signatureRedisDAO) { + this(new ApiSignatureVerifier(signatureRedisDAO), new ApiSignatureProperties()); + } + + public ApiSignatureAspect(ApiSignatureVerifier verifier, ApiSignatureProperties properties) { + this.verifier = verifier; + this.properties = properties; + } @Before("@annotation(signature)") public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) { - // 1. 验证通过,直接结束 - if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) { - return; - } - - // 2. 验证不通过,抛出异常 - log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(), - joinPoint.getArgs()); - throw new ServiceException(BAD_REQUEST.getCode(), - StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg())); + HttpServletRequest request = Objects.requireNonNull(ServletUtils.getRequest()); + ApiSignatureRule rule = ApiSignatureRule.from(signature, properties); + verifier.verify(rule, request); + log.debug("[beforePointCut][方法{} 参数({}) 签名校验通过]", joinPoint.getSignature(), joinPoint.getArgs()); } public boolean verifySignature(ApiSignature signature, HttpServletRequest request) { - // 1.1 校验 Header - if (!verifyHeaders(signature, request)) { - return false; - } - // 1.2 校验 appId 是否能获取到对应的 appSecret - String appId = request.getHeader(signature.appId()); - String appSecret = signatureRedisDAO.getAppSecret(appId); - Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId); - - // 2. 校验签名【重要!】 - String clientSignature = request.getHeader(signature.sign()); // 客户端签名 - String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串 - String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名 - if (ObjUtil.notEqual(clientSignature, serverSignature)) { - return false; - } - - // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) - String nonce = request.getHeader(signature.nonce()); - if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit()))) { - String timestamp = request.getHeader(signature.timestamp()); - log.info("[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]", appId, timestamp, nonce, clientSignature); - throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), "存在重复请求"); - } + ApiSignatureRule rule = ApiSignatureRule.from(signature, properties); + verifier.verify(rule, request); return true; } - /** - * 校验请求头加签参数 - *

- * 1. appId 是否为空 - * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟 - * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了 - * 4. sign 是否为空 - * - * @param signature signature - * @param request request - * @return 是否校验 Header 通过 - */ - private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) { - // 1. 非空校验 - String appId = request.getHeader(signature.appId()); - if (StrUtil.isBlank(appId)) { - return false; - } - String timestamp = request.getHeader(signature.timestamp()); - if (StrUtil.isBlank(timestamp)) { - return false; - } - String nonce = request.getHeader(signature.nonce()); - if (StrUtil.length(nonce) < 10) { - return false; - } - String sign = request.getHeader(signature.sign()); - if (StrUtil.isBlank(sign)) { - return false; - } - - // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值) - long expireTime = signature.timeUnit().toMillis(signature.timeout()); - long requestTimestamp = Long.parseLong(timestamp); - long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); - if (timestampDisparity > expireTime) { - return false; - } - - // 3. 检查 nonce 是否存在,有且仅能使用一次 - return signatureRedisDAO.getNonce(appId, nonce) == null; - } - - /** - * 构建签名字符串 - *

- * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥 - * - * @param signature signature - * @param request request - * @param appSecret appSecret - * @return 签名字符串 - */ - private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) { - SortedMap parameterMap = getRequestParameterMap(request); // 请求头 - SortedMap headerMap = getRequestHeaderMap(signature, request); // 请求参数 - String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体 - return MapUtil.join(parameterMap, "&", "=") - + requestBody - + MapUtil.join(headerMap, "&", "=") - + appSecret; - } - - /** - * 获取请求头加签参数 Map - * - * @param request 请求 - * @param signature 签名注解 - * @return signature params - */ - private static SortedMap getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) { - SortedMap sortedMap = new TreeMap<>(); - sortedMap.put(signature.appId(), request.getHeader(signature.appId())); - sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp())); - sortedMap.put(signature.nonce(), request.getHeader(signature.nonce())); - return sortedMap; - } - - /** - * 获取请求参数 Map - * - * @param request 请求 - * @return queryParams - */ - private static SortedMap getRequestParameterMap(HttpServletRequest request) { - SortedMap sortedMap = new TreeMap<>(); - for (Map.Entry entry : request.getParameterMap().entrySet()) { - sortedMap.put(entry.getKey(), entry.getValue()[0]); - } - return sortedMap; - } - } \ No newline at end of file diff --git a/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/config/ApiSignatureProperties.java b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/config/ApiSignatureProperties.java new file mode 100644 index 00000000..d2158a89 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/config/ApiSignatureProperties.java @@ -0,0 +1,68 @@ +package com.zt.plat.framework.signature.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * API 签名配置 + */ +@Data +@ConfigurationProperties(prefix = "zt.api-signature") +public class ApiSignatureProperties { + + /** + * 是否开启全局签名校验 + */ + private boolean enabled = true; + + /** + * 签名有效期 + */ + private int timeout = 60; + + /** + * 时间单位 + */ + private TimeUnit timeUnit = TimeUnit.SECONDS; + + /** + * 校验失败时的提示信息 + */ + private String message = "签名不正确"; + + /** + * 请求头:appId + */ + private String appId = "appId"; + + /** + * 请求头:timestamp + */ + private String timestamp = "timestamp"; + + /** + * 请求头:nonce + */ + private String nonce = "nonce"; + + /** + * 请求头:sign + */ + private String sign = "sign"; + + /** + * 需要进行签名校验的路径,默认全量 + */ + private List includePaths = new ArrayList<>(Arrays.asList("/**")); + + /** + * 无需签名校验的路径 + */ + private List excludePaths = new ArrayList<>(Arrays.asList("/error", "/swagger-ui/**", "/v3/api-docs/**")); + +} \ No newline at end of file diff --git a/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/model/ApiSignatureRule.java b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/model/ApiSignatureRule.java new file mode 100644 index 00000000..1c71c962 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/model/ApiSignatureRule.java @@ -0,0 +1,50 @@ +package com.zt.plat.framework.signature.core.model; + +import cn.hutool.core.util.StrUtil; +import com.zt.plat.framework.signature.core.annotation.ApiSignature; +import com.zt.plat.framework.signature.core.config.ApiSignatureProperties; +import lombok.Builder; +import lombok.Getter; + +import java.util.concurrent.TimeUnit; + +/** + * 签名校验规则 + */ +@Getter +@Builder +public class ApiSignatureRule { + + private final int timeout; + private final TimeUnit timeUnit; + private final String message; + private final String appId; + private final String timestamp; + private final String nonce; + private final String sign; + + public static ApiSignatureRule from(ApiSignatureProperties properties) { + return ApiSignatureRule.builder() + .timeout(properties.getTimeout()) + .timeUnit(properties.getTimeUnit()) + .message(properties.getMessage()) + .appId(properties.getAppId()) + .timestamp(properties.getTimestamp()) + .nonce(properties.getNonce()) + .sign(properties.getSign()) + .build(); + } + + public static ApiSignatureRule from(ApiSignature signature, ApiSignatureProperties defaults) { + return ApiSignatureRule.builder() + .timeout(signature.timeout()) + .timeUnit(signature.timeUnit()) + .message(StrUtil.blankToDefault(signature.message(), defaults.getMessage())) + .appId(signature.appId()) + .timestamp(signature.timestamp()) + .nonce(signature.nonce()) + .sign(signature.sign()) + .build(); + } + +} \ No newline at end of file diff --git a/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/web/ApiSignatureHandlerInterceptor.java b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/web/ApiSignatureHandlerInterceptor.java new file mode 100644 index 00000000..faf528ff --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-protection/src/main/java/com/zt/plat/framework/signature/core/web/ApiSignatureHandlerInterceptor.java @@ -0,0 +1,80 @@ +package com.zt.plat.framework.signature.core.web; + +import com.zt.plat.framework.signature.core.ApiSignatureVerifier; +import com.zt.plat.framework.signature.core.annotation.ApiSignature; +import com.zt.plat.framework.signature.core.config.ApiSignatureProperties; +import com.zt.plat.framework.signature.core.model.ApiSignatureRule; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.CollectionUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.util.UrlPathHelper; + +import java.util.List; + +/** + * 全局 API 签名拦截器 + */ +@RequiredArgsConstructor +public class ApiSignatureHandlerInterceptor implements HandlerInterceptor { + + private final ApiSignatureVerifier verifier; + private final ApiSignatureProperties properties; + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + private final UrlPathHelper urlPathHelper = new UrlPathHelper(); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (!properties.isEnabled()) { + return true; + } + if (!(handler instanceof HandlerMethod handlerMethod)) { + return true; + } + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + String lookupPath = urlPathHelper.getLookupPathForRequest(request); + if (shouldSkip(lookupPath)) { + return true; + } + + ApiSignatureRule rule = createRule(handlerMethod); + verifier.verify(rule, request); + return true; + } + + private boolean shouldSkip(String path) { + List includePaths = properties.getIncludePaths(); + if (!CollectionUtils.isEmpty(includePaths)) { + boolean matched = includePaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path)); + if (!matched) { + return true; + } + } + List excludePaths = properties.getExcludePaths(); + if (!CollectionUtils.isEmpty(excludePaths)) { + for (String pattern : excludePaths) { + if (pathMatcher.match(pattern, path)) { + return true; + } + } + } + return false; + } + + private ApiSignatureRule createRule(HandlerMethod handlerMethod) { + ApiSignature signature = handlerMethod.getMethodAnnotation(ApiSignature.class); + if (signature != null) { + return ApiSignatureRule.from(signature, properties); + } + signature = handlerMethod.getBeanType().getAnnotation(ApiSignature.class); + if (signature != null) { + return ApiSignatureRule.from(signature, properties); + } + return ApiSignatureRule.from(properties); + } +} diff --git a/zt-framework/zt-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/zt-framework/zt-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 96ad764d..3ed881de 100644 --- a/zt-framework/zt-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/zt-framework/zt-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,4 +1,3 @@ com.zt.plat.framework.idempotent.config.ZtIdempotentConfiguration com.zt.plat.framework.lock4j.config.ZtLock4jConfiguration com.zt.plat.framework.ratelimiter.config.ZtRateLimiterConfiguration -com.zt.plat.framework.signature.config.ZtApiSignatureAutoConfiguration \ No newline at end of file diff --git a/zt-framework/zt-spring-boot-starter-protection/src/test/java/com/zt/plat/framework/signature/core/ApiSignatureTest.java b/zt-framework/zt-spring-boot-starter-protection/src/test/java/com/zt/plat/framework/signature/core/ApiSignatureTest.java index 3d93d079..163bd03b 100644 --- a/zt-framework/zt-spring-boot-starter-protection/src/test/java/com/zt/plat/framework/signature/core/ApiSignatureTest.java +++ b/zt-framework/zt-spring-boot-starter-protection/src/test/java/com/zt/plat/framework/signature/core/ApiSignatureTest.java @@ -3,13 +3,13 @@ package com.zt.plat.framework.signature.core; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.digest.DigestUtil; -import com.zt.plat.framework.signature.core.annotation.ApiSignature; -import com.zt.plat.framework.signature.core.aop.ApiSignatureAspect; +import com.zt.plat.framework.signature.core.ApiSignatureVerifier; +import com.zt.plat.framework.signature.core.config.ApiSignatureProperties; +import com.zt.plat.framework.signature.core.model.ApiSignatureRule; import com.zt.plat.framework.signature.core.redis.ApiSignatureRedisDAO; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -28,9 +28,6 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class ApiSignatureTest { - @InjectMocks - private ApiSignatureAspect apiSignatureAspect; - @Mock private ApiSignatureRedisDAO signatureRedisDAO; @@ -45,13 +42,16 @@ public class ApiSignatureTest { String sign = DigestUtil.sha256Hex(signString); // 准备参数 - ApiSignature apiSignature = mock(ApiSignature.class); - when(apiSignature.appId()).thenReturn("appId"); - when(apiSignature.timestamp()).thenReturn("timestamp"); - when(apiSignature.nonce()).thenReturn("nonce"); - when(apiSignature.sign()).thenReturn("sign"); - when(apiSignature.timeout()).thenReturn(60); - when(apiSignature.timeUnit()).thenReturn(TimeUnit.SECONDS); + ApiSignatureProperties properties = new ApiSignatureProperties(); + ApiSignatureRule apiSignature = ApiSignatureRule.builder() + .appId("appId") + .timestamp("timestamp") + .nonce("nonce") + .sign("sign") + .timeout(60) + .timeUnit(TimeUnit.SECONDS) + .message(properties.getMessage()) + .build(); HttpServletRequest request = mock(HttpServletRequest.class); when(request.getHeader(eq("appId"))).thenReturn(appId); when(request.getHeader(eq("timestamp"))).thenReturn(String.valueOf(timestamp)); @@ -62,11 +62,13 @@ public class ApiSignatureTest { when(request.getContentType()).thenReturn("application/json"); when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test"))); // mock 方法 + when(signatureRedisDAO.getNonce(eq(appId), eq(nonce))).thenReturn(null); when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret); when(signatureRedisDAO.setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS))).thenReturn(true); // 调用 - boolean result = apiSignatureAspect.verifySignature(apiSignature, request); + ApiSignatureVerifier verifier = new ApiSignatureVerifier(signatureRedisDAO); + boolean result = verifier.verify(apiSignature, request); // 断言结果 assertTrue(result); } diff --git a/zt-gateway/src/main/java/com/zt/plat/gateway/filter/security/TokenAuthenticationFilter.java b/zt-gateway/src/main/java/com/zt/plat/gateway/filter/security/TokenAuthenticationFilter.java index c75714f5..8198ee0d 100644 --- a/zt-gateway/src/main/java/com/zt/plat/gateway/filter/security/TokenAuthenticationFilter.java +++ b/zt-gateway/src/main/java/com/zt/plat/gateway/filter/security/TokenAuthenticationFilter.java @@ -44,6 +44,8 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered { private static final TypeReference> CHECK_RESULT_TYPE_REFERENCE = new TypeReference>() {}; + private static final String ADMIN_DATABUS_PORTAL_PREFIX = "/admin-api/databus/api/portal"; + /** * 空的 LoginUser 的结果 * @@ -85,6 +87,13 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered { // 移除 login-user 的请求头,避免伪造模拟 exchange = SecurityFrameworkUtils.removeLoginUser(exchange); + // API Portal 通过网关访问时无需认证,直接放行 + String rawPath = exchange.getRequest().getURI().getRawPath(); + if (rawPath != null && (rawPath.equals(ADMIN_DATABUS_PORTAL_PREFIX) + || rawPath.startsWith(ADMIN_DATABUS_PORTAL_PREFIX + "/"))) { + return chain.filter(exchange); + } + // 情况一,如果没有 Token 令牌,则直接继续 filter String token = SecurityFrameworkUtils.obtainAuthorization(exchange); if (StrUtil.isEmpty(token)) { diff --git a/zt-module-ai/zt-module-ai-server/src/main/resources/application.yaml b/zt-module-ai/zt-module-ai-server/src/main/resources/application.yaml index 73d96ed9..53a15dc8 100644 --- a/zt-module-ai/zt-module-ai-server/src/main/resources/application.yaml +++ b/zt-module-ai/zt-module-ai-server/src/main/resources/application.yaml @@ -212,8 +212,8 @@ zt: exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: ai 模块 + description: 提供ai功能 version: ${zt.info.version} tenant: # 多租户相关配置项 enable: true diff --git a/zt-module-bpm/zt-module-bpm-server/src/main/resources/application.yaml b/zt-module-bpm/zt-module-bpm-server/src/main/resources/application.yaml index 07020c50..adfbf58f 100644 --- a/zt-module-bpm/zt-module-bpm-server/src/main/resources/application.yaml +++ b/zt-module-bpm/zt-module-bpm-server/src/main/resources/application.yaml @@ -141,8 +141,8 @@ zt: exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: 流程模块 + description: 提供流程模块功能 version: ${zt.info.version} tenant: # 多租户相关配置项 enable: true diff --git a/zt-module-databus/zt-module-databus-server/pom.xml b/zt-module-databus/zt-module-databus-server/pom.xml index 0d51180c..008a387f 100644 --- a/zt-module-databus/zt-module-databus-server/pom.xml +++ b/zt-module-databus/zt-module-databus-server/pom.xml @@ -140,11 +140,6 @@ org.springframework.integration spring-integration-scripting - - org.springframework.retry - spring-retry - - org.springframework.boot diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiClientCredentialController.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiClientCredentialController.java new file mode 100644 index 00000000..3682b7d8 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiClientCredentialController.java @@ -0,0 +1,79 @@ +package com.zt.plat.module.databus.controller.admin.gateway; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiClientCredentialConvert; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialPageReqVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialSaveReqVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialSimpleRespVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO; +import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static com.zt.plat.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - API 客户端凭证") +@RestController +@RequestMapping("/databus/gateway/credential") +@RequiredArgsConstructor +@Validated +public class ApiClientCredentialController { + + private final ApiClientCredentialService credentialService; + + @GetMapping("/page") + @Operation(summary = "分页查询客户端凭证") + public CommonResult> page(ApiClientCredentialPageReqVO reqVO) { + PageResult page = credentialService.getPage(reqVO); + return success(ApiClientCredentialConvert.INSTANCE.convertPage(page)); + } + + @GetMapping("/get") + @Operation(summary = "查询凭证详情") + public CommonResult get(@RequestParam("id") Long id) { + ApiClientCredentialDO credential = credentialService.get(id); + return success(ApiClientCredentialConvert.INSTANCE.convert(credential)); + } + + @PostMapping("/create") + @Operation(summary = "新增客户端凭证") + public CommonResult create(@Valid @RequestBody ApiClientCredentialSaveReqVO reqVO) { + return success(credentialService.create(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新客户端凭证") + public CommonResult update(@Valid @RequestBody ApiClientCredentialSaveReqVO reqVO) { + credentialService.update(reqVO); + return success(Boolean.TRUE); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除客户端凭证") + public CommonResult delete(@RequestParam("id") Long id) { + credentialService.delete(id); + return success(Boolean.TRUE); + } + + @GetMapping("/list-simple") + @Operation(summary = "获取启用的凭证列表(精简)") + public CommonResult> listSimple() { + List list = credentialService.listEnabled(); + return success(ApiClientCredentialConvert.INSTANCE.convertSimpleList(list)); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayController.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayController.java index e7a44307..5f451796 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayController.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayController.java @@ -1,25 +1,20 @@ package com.zt.plat.module.databus.controller.admin.gateway; -import com.zt.plat.framework.common.exception.ServiceException; import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiDefinitionConvert; import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO; import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionDetailRespVO; -import com.zt.plat.module.databus.framework.integration.gateway.core.ApiFlowDispatcher; +import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayExecutionService; import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; -import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.util.StringUtils; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import static com.zt.plat.framework.common.pojo.CommonResult.success; @@ -30,44 +25,13 @@ import static com.zt.plat.framework.common.pojo.CommonResult.success; @RequiredArgsConstructor public class ApiGatewayController { - private final ApiFlowDispatcher apiFlowDispatcher; + private final ApiGatewayExecutionService executionService; private final ApiDefinitionService apiDefinitionService; @PostMapping(value = "/invoke", consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "测试调用 API 编排") - public CommonResult invoke(@RequestBody ApiGatewayInvokeReqVO reqVO) { - ApiInvocationContext context = ApiInvocationContext.create(); - context.setApiCode(reqVO.getApiCode()); - context.setApiVersion(reqVO.getVersion()); - context.setRequestBody(reqVO.getPayload()); - if (reqVO.getHeaders() != null) { - context.getRequestHeaders().putAll(reqVO.getHeaders()); - } - if (reqVO.getQueryParams() != null) { - context.getRequestQueryParams().putAll(reqVO.getQueryParams()); - } - - ApiInvocationContext responseContext = context; - try { - responseContext = apiFlowDispatcher.dispatch(reqVO.getApiCode(), reqVO.getVersion(), context); - } catch (ServiceException ex) { - handleServiceException(responseContext, ex); - } catch (Exception ex) { - handleUnexpectedException(responseContext, ex); - } - - int status = responseContext.getResponseStatus() != null ? responseContext.getResponseStatus() : HttpStatus.OK.value(); - String message = StringUtils.hasText(responseContext.getResponseMessage()) - ? responseContext.getResponseMessage() - : HttpStatus.valueOf(status).getReasonPhrase(); - - ApiGatewayResponse envelope = ApiGatewayResponse.builder() - .code(status >= 200 && status < 400 ? "SUCCESS" : "ERROR") - .message(message) - .data(responseContext.getResponseBody()) - .traceId(responseContext.getRequestId()) - .build(); - return success(envelope); + public ResponseEntity invoke(@RequestBody ApiGatewayInvokeReqVO reqVO) { + return executionService.invokeForDebug(reqVO); } @GetMapping("/definitions") @@ -79,29 +43,4 @@ public class ApiGatewayController { return success(definitions); } - private void handleServiceException(ApiInvocationContext context, ServiceException ex) { - String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API 调用失败"; - context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - context.setResponseMessage(message); - Map body = new HashMap<>(); - if (ex.getCode() != null) { - body.put("errorCode", ex.getCode()); - } - body.put("errorMessage", message); - context.setResponseBody(body); - } - - private void handleUnexpectedException(ApiInvocationContext context, Exception ex) { - String message = StringUtils.hasText(ex.getMessage()) - ? ex.getMessage() - : ex.getCause() != null && StringUtils.hasText(ex.getCause().getMessage()) - ? ex.getCause().getMessage() - : "API invocation encountered an unexpected error"; - context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - context.setResponseMessage(message); - Map body = new HashMap<>(); - body.put("errorMessage", message); - body.put("exception", ex.getClass().getSimpleName()); - context.setResponseBody(body); - } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiPolicyAuthController.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiPolicyAuthController.java deleted file mode 100644 index 9547c46b..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiPolicyAuthController.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.zt.plat.module.databus.controller.admin.gateway; - -import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; -import com.zt.plat.framework.common.pojo.CommonResult; -import com.zt.plat.framework.common.pojo.PageResult; -import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiPolicyAuthConvert; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyRespVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySimpleRespVO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; -import com.zt.plat.module.databus.service.gateway.ApiPolicyAuthService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -import static com.zt.plat.framework.common.pojo.CommonResult.success; -import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_NOT_FOUND; - -@Tag(name = "管理后台 - 网关认证策略") -@RestController -@RequestMapping("/databus/gateway/policy/auth") -@RequiredArgsConstructor -@Validated -public class ApiPolicyAuthController { - - private final ApiPolicyAuthService authService; - - @GetMapping("/page") - @Operation(summary = "分页查询认证策略") - public CommonResult> getAuthPolicyPage(@Valid ApiPolicyPageReqVO reqVO) { - PageResult pageResult = authService.getPage(reqVO); - return success(ApiPolicyAuthConvert.INSTANCE.convertPage(pageResult)); - } - - @GetMapping("/{id}") - @Operation(summary = "查询认证策略详情") - public CommonResult getAuthPolicy(@PathVariable("id") Long id) { - ApiPolicyAuthDO policy = authService.get(id) - .orElseThrow(() -> ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND)); - return success(ApiPolicyAuthConvert.INSTANCE.convert(policy)); - } - - @GetMapping("/simple-list") - @Operation(summary = "获取认证策略精简列表") - public CommonResult> getAuthPolicySimpleList() { - List list = authService.getSimpleList(); - return success(ApiPolicyAuthConvert.INSTANCE.convertSimpleList(list)); - } - - @PostMapping - @Operation(summary = "创建认证策略") - public CommonResult createAuthPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) { - Long id = authService.create(reqVO); - return success(id); - } - - @PutMapping - @Operation(summary = "更新认证策略") - public CommonResult updateAuthPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) { - authService.update(reqVO); - return success(Boolean.TRUE); - } - - @DeleteMapping("/{id}") - @Operation(summary = "删除认证策略") - public CommonResult deleteAuthPolicy(@PathVariable("id") Long id) { - authService.delete(id); - return success(Boolean.TRUE); - } - -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiClientCredentialConvert.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiClientCredentialConvert.java new file mode 100644 index 00000000..29e1e821 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiClientCredentialConvert.java @@ -0,0 +1,41 @@ +package com.zt.plat.module.databus.controller.admin.gateway.convert; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialRespVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialSimpleRespVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.stream.Collectors; + +@Mapper +public interface ApiClientCredentialConvert { + + ApiClientCredentialConvert INSTANCE = Mappers.getMapper(ApiClientCredentialConvert.class); + + ApiClientCredentialRespVO convert(ApiClientCredentialDO bean); + + List convertList(List list); + + default PageResult convertPage(PageResult page) { + if (page == null) { + return PageResult.empty(); + } + PageResult result = new PageResult<>(); + result.setList(convertList(page.getList())); + result.setTotal(page.getTotal()); + return result; + } + + default List convertSimpleList(List list) { + return list == null ? List.of() : list.stream().map(item -> { + ApiClientCredentialSimpleRespVO vo = new ApiClientCredentialSimpleRespVO(); + vo.setId(item.getId()); + vo.setAppId(item.getAppId()); + vo.setAppName(item.getAppName()); + return vo; + }).collect(Collectors.toList()); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiPolicyAuthConvert.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiPolicyAuthConvert.java deleted file mode 100644 index 3220d221..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiPolicyAuthConvert.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.zt.plat.module.databus.controller.admin.gateway.convert; - -import com.zt.plat.framework.common.pojo.PageResult; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyRespVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySimpleRespVO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; -import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; - -import java.util.List; - -@Mapper -public interface ApiPolicyAuthConvert { - - ApiPolicyAuthConvert INSTANCE = Mappers.getMapper(ApiPolicyAuthConvert.class); - - ApiPolicyRespVO convert(ApiPolicyAuthDO bean); - - List convertList(List list); - - PageResult convertPage(PageResult page); - - List convertSimpleList(List list); - -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialPageReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialPageReqVO.java new file mode 100644 index 00000000..e11f09c0 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialPageReqVO.java @@ -0,0 +1,19 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.credential; + +import com.zt.plat.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - API 客户端凭证分页查询 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ApiClientCredentialPageReqVO extends PageParam { + + @Schema(description = "关键字,匹配 appId 或名称", example = "databus-app") + private String keyword; + + @Schema(description = "是否启用") + private Boolean enabled; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialRespVO.java new file mode 100644 index 00000000..ceb9d621 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialRespVO.java @@ -0,0 +1,42 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.credential; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - API 客户端凭证 Response VO") +@Data +public class ApiClientCredentialRespVO { + + @Schema(description = "记录编号", example = "1024") + private Long id; + + @Schema(description = "应用标识", example = "databus-app") + private String appId; + + @Schema(description = "应用名称", example = "数据总线默认应用") + private String appName; + + @Schema(description = "加密密钥", example = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=") + private String encryptionKey; + + @Schema(description = "加密算法", example = "AES") + private String encryptionType; + + @Schema(description = "签名算法", example = "MD5") + private String signatureType; + + @Schema(description = "是否启用", example = "true") + private Boolean enabled; + + @Schema(description = "备注", example = "默认应用凭证") + private String remark; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "最后更新时间") + private LocalDateTime updateTime; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSaveReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSaveReqVO.java new file mode 100644 index 00000000..15765118 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSaveReqVO.java @@ -0,0 +1,41 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.credential; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - API 客户端凭证保存 Request VO") +@Data +public class ApiClientCredentialSaveReqVO { + + @Schema(description = "记录编号,仅更新时必填", example = "1024") + private Long id; + + @Schema(description = "应用标识", example = "databus-app") + @NotBlank(message = "应用标识不能为空") + private String appId; + + @Schema(description = "应用名称", example = "数据总线默认应用") + private String appName; + + @Schema(description = "加密密钥", example = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=") + @NotBlank(message = "加密密钥不能为空") + private String encryptionKey; + + @Schema(description = "加密算法", example = "AES") + @NotBlank(message = "加密算法不能为空") + private String encryptionType; + + @Schema(description = "签名算法", example = "MD5") + @NotBlank(message = "签名算法不能为空") + private String signatureType; + + @Schema(description = "是否启用", example = "true") + @NotNull(message = "启用状态不能为空") + private Boolean enabled; + + @Schema(description = "备注", example = "默认应用凭证") + private String remark; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSimpleRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSimpleRespVO.java new file mode 100644 index 00000000..103f5ba7 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSimpleRespVO.java @@ -0,0 +1,19 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.credential; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - API 客户端凭证精简 Response VO") +@Data +public class ApiClientCredentialSimpleRespVO { + + @Schema(description = "记录编号", example = "1024") + private Long id; + + @Schema(description = "应用标识", example = "databus-app") + private String appId; + + @Schema(description = "应用名称", example = "数据总线默认应用") + private String appName; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionDetailRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionDetailRespVO.java index dda4cf3b..c64ba1ec 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionDetailRespVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionDetailRespVO.java @@ -26,30 +26,18 @@ public class ApiDefinitionDetailRespVO { @Schema(description = "HTTP 方法", example = "POST") private String httpMethod; - @Schema(description = "URI 模板", example = "/external/order/create") - private String uriPattern; - @Schema(description = "状态", example = "1") private Integer status; - @Schema(description = "是否灰度") - private Boolean greyReleased; - @Schema(description = "描述") private String description; - @Schema(description = "认证策略编号") - private Long authPolicyId; - @Schema(description = "限流策略编号") private Long rateLimitId; @Schema(description = "响应模板(JSON)") private String responseTemplate; - @Schema(description = "缓存策略(JSON)") - private String cacheStrategy; - @Schema(description = "创建时间") private LocalDateTime createTime; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionPageReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionPageReqVO.java index cf5d82f4..3f6025ce 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionPageReqVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionPageReqVO.java @@ -10,7 +10,7 @@ import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) public class ApiDefinitionPageReqVO extends PageParam { - @Schema(description = "关键字,匹配编码/描述/URI", example = "order") + @Schema(description = "关键字,匹配编码或描述", example = "order") private String keyword; @Schema(description = "API 状态", example = "1") @@ -19,7 +19,4 @@ public class ApiDefinitionPageReqVO extends PageParam { @Schema(description = "HTTP 方法", example = "POST") private String httpMethod; - @Schema(description = "是否灰度", example = "true") - private Boolean greyReleased; - } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSaveReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSaveReqVO.java index f9239581..afd54dca 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSaveReqVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSaveReqVO.java @@ -29,10 +29,6 @@ public class ApiDefinitionSaveReqVO { @NotBlank(message = "HTTP 方法不能为空") private String httpMethod; - @Schema(description = "URI 模板", example = "/external/order/create") - @NotBlank(message = "URI 模板不能为空") - private String uriPattern; - @Schema(description = "API 状态", example = "1") @NotNull(message = "API 状态不能为空") private Integer status; @@ -40,21 +36,12 @@ public class ApiDefinitionSaveReqVO { @Schema(description = "描述") private String description; - @Schema(description = "认证策略编号") - private Long authPolicyId; - @Schema(description = "限流策略编号") private Long rateLimitId; @Schema(description = "响应模板(JSON)") private String responseTemplate; - @Schema(description = "缓存策略(JSON)") - private String cacheStrategy; - - @Schema(description = "是否开启灰度发布") - private Boolean greyReleased; - @Schema(description = "API 级别变换列表") @Valid private List apiLevelTransforms = new ArrayList<>(); diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionStepRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionStepRespVO.java index e6f7b71d..fba77079 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionStepRespVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionStepRespVO.java @@ -37,9 +37,6 @@ public class ApiDefinitionStepRespVO { @Schema(description = "超时时间(毫秒)") private Long timeout; - @Schema(description = "重试策略(JSON)") - private String retryStrategy; - @Schema(description = "降级策略(JSON)") private String fallbackStrategy; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionStepSaveReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionStepSaveReqVO.java index 650430ac..2f66ed5e 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionStepSaveReqVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionStepSaveReqVO.java @@ -39,9 +39,6 @@ public class ApiDefinitionStepSaveReqVO { @Schema(description = "超时时间(毫秒)", example = "5000") private Long timeout; - @Schema(description = "重试策略(JSON)") - private String retryStrategy; - @Schema(description = "降级策略(JSON)") private String fallbackStrategy; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSummaryRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSummaryRespVO.java index 75e528cb..865b06d9 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSummaryRespVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiDefinitionSummaryRespVO.java @@ -21,15 +21,9 @@ public class ApiDefinitionSummaryRespVO { @Schema(description = "HTTP 方法", example = "POST") private String httpMethod; - @Schema(description = "URI 模板", example = "/external/order/create") - private String uriPattern; - @Schema(description = "状态", example = "1") private Integer status; - @Schema(description = "是否灰度", example = "true") - private Boolean greyReleased; - @Schema(description = "描述") private String description; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiClientCredentialDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiClientCredentialDO.java new file mode 100644 index 00000000..76d0ad44 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiClientCredentialDO.java @@ -0,0 +1,37 @@ +package com.zt.plat.module.databus.dal.dataobject.gateway; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zt.plat.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * API 客户端凭证,用于维护 appId 与加解密配置的关联关系。 + */ +@Data +@TableName("databus_api_client_credential") +@KeySequence("databus_api_client_credential_seq") +@EqualsAndHashCode(callSuper = true) +public class ApiClientCredentialDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private String appId; + + private String appName; + + private String encryptionKey; + + private String encryptionType; + + private String signatureType; + + private Boolean enabled; + + private String remark; + +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionDO.java index 596bb40d..af287c02 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionDO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionDO.java @@ -24,8 +24,6 @@ public class ApiDefinitionDO extends TenantBaseDO { private String apiCode; - private String uriPattern; - private String httpMethod; private String version; @@ -37,16 +35,10 @@ public class ApiDefinitionDO extends TenantBaseDO { private String description; - private Long authPolicyId; - private Long rateLimitId; private String responseTemplate; - private String cacheStrategy; - private LocalDateTime updatedAt; - private Boolean greyReleased; - } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiPolicyAuthDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiPolicyAuthDO.java deleted file mode 100644 index 90054af6..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiPolicyAuthDO.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.zt.plat.module.databus.dal.dataobject.gateway; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.zt.plat.framework.tenant.core.db.TenantBaseDO; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * Authentication policy definition. - */ -@TableName("databus_policy_auth") -@KeySequence("databus_policy_auth_seq") -@Data -@EqualsAndHashCode(callSuper = true) -public class ApiPolicyAuthDO extends TenantBaseDO { - - @TableId(type = IdType.ASSIGN_ID) - private Long id; - - private String name; - - private String type; - - private String config; - - private String description; - -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiStepDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiStepDO.java index b0f17529..74af0037 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiStepDO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiStepDO.java @@ -38,8 +38,6 @@ public class ApiStepDO extends TenantBaseDO { private Long timeout; - private String retryStrategy; - private String fallbackStrategy; private String conditionExpr; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiClientCredentialMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiClientCredentialMapper.java new file mode 100644 index 00000000..39a7bbcf --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiClientCredentialMapper.java @@ -0,0 +1,47 @@ +package com.zt.plat.module.databus.dal.mysql.gateway; + +import cn.hutool.core.util.StrUtil; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialPageReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Optional; + +@Mapper +public interface ApiClientCredentialMapper extends BaseMapperX { + + default Optional selectByAppId(String appId) { + if (StrUtil.isBlank(appId)) { + return Optional.empty(); + } + LambdaQueryWrapperX query = new LambdaQueryWrapperX<>(); + query.eq(ApiClientCredentialDO::getAppId, appId) + .eq(ApiClientCredentialDO::getDeleted, false); + return Optional.ofNullable(selectOne(query)); + } + + default PageResult selectPage(ApiClientCredentialPageReqVO reqVO) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX<>(); + if (StrUtil.isNotBlank(reqVO.getKeyword())) { + String keyword = reqVO.getKeyword(); + query.and(wrapper -> wrapper.like(ApiClientCredentialDO::getAppId, keyword) + .or().like(ApiClientCredentialDO::getAppName, keyword)); + } + query.eqIfPresent(ApiClientCredentialDO::getEnabled, reqVO.getEnabled()) + .eq(ApiClientCredentialDO::getDeleted, false) + .orderByDesc(ApiClientCredentialDO::getUpdateTime) + .orderByDesc(ApiClientCredentialDO::getId); + return selectPage(reqVO, query); + } + + default List selectEnabledList() { + return selectList(new LambdaQueryWrapperX() + .eq(ApiClientCredentialDO::getEnabled, true) + .eq(ApiClientCredentialDO::getDeleted, false) + .orderByAsc(ApiClientCredentialDO::getAppId)); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionMapper.java index d4278f4a..8d2e2514 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionMapper.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionMapper.java @@ -31,26 +31,15 @@ public interface ApiDefinitionMapper extends BaseMapperX { if (StrUtil.isNotBlank(reqVO.getKeyword())) { String keyword = reqVO.getKeyword(); query.and(wrapper -> wrapper.like(ApiDefinitionDO::getApiCode, keyword) - .or().like(ApiDefinitionDO::getDescription, keyword) - .or().like(ApiDefinitionDO::getUriPattern, keyword)); + .or().like(ApiDefinitionDO::getDescription, keyword)); } - query.eqIfPresent(ApiDefinitionDO::getStatus, reqVO.getStatus()) - .eqIfPresent(ApiDefinitionDO::getHttpMethod, reqVO.getHttpMethod()) -// .eqIfPresent(ApiDefinitionDO::getGreyReleased, reqVO.getGreyReleased()) - .orderByDesc(ApiDefinitionDO::getUpdateTime) + query.eqIfPresent(ApiDefinitionDO::getStatus, reqVO.getStatus()) + .eqIfPresent(ApiDefinitionDO::getHttpMethod, reqVO.getHttpMethod()) + .orderByDesc(ApiDefinitionDO::getUpdateTime) .orderByDesc(ApiDefinitionDO::getId); return selectPage(reqVO, query); } - default Long selectCountByAuthPolicyId(Long policyId) { - if (policyId == null) { - return 0L; - } - return selectCount(new LambdaQueryWrapperX() - .eq(ApiDefinitionDO::getAuthPolicyId, policyId) - .eq(ApiDefinitionDO::getDeleted, false)); - } - default Long selectCountByRateLimitPolicyId(Long policyId) { if (policyId == null) { return 0L; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiPolicyAuthMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiPolicyAuthMapper.java deleted file mode 100644 index b1bf14ba..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiPolicyAuthMapper.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.zt.plat.module.databus.dal.mysql.gateway; - -import cn.hutool.core.util.StrUtil; -import com.zt.plat.framework.common.pojo.PageResult; -import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; -import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -@Mapper -public interface ApiPolicyAuthMapper extends BaseMapperX { - - default PageResult selectPage(ApiPolicyPageReqVO reqVO) { - LambdaQueryWrapperX query = new LambdaQueryWrapperX<>(); - if (StrUtil.isNotBlank(reqVO.getKeyword())) { - String keyword = reqVO.getKeyword(); - query.and(wrapper -> wrapper.like(ApiPolicyAuthDO::getName, keyword) - .or().like(ApiPolicyAuthDO::getDescription, keyword)); - } - query.eqIfPresent(ApiPolicyAuthDO::getType, reqVO.getType()) - .eq(ApiPolicyAuthDO::getDeleted, false) - .orderByDesc(ApiPolicyAuthDO::getUpdateTime) - .orderByDesc(ApiPolicyAuthDO::getId); - return selectPage(reqVO, query); - } - - default List selectSimpleList() { - return selectList(new LambdaQueryWrapperX() - .eq(ApiPolicyAuthDO::getDeleted, false) - .orderByDesc(ApiPolicyAuthDO::getUpdateTime) - .orderByDesc(ApiPolicyAuthDO::getId)); - } -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/enums/gateway/ApiStepTypeEnum.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/enums/gateway/ApiStepTypeEnum.java index 589a49f1..28416514 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/enums/gateway/ApiStepTypeEnum.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/enums/gateway/ApiStepTypeEnum.java @@ -10,9 +10,10 @@ import lombok.Getter; @Getter public enum ApiStepTypeEnum { + START, HTTP, RPC, SCRIPT, - FLOW; + END; } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/ApiGatewayProperties.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/ApiGatewayProperties.java index ac37137a..036f41d3 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/ApiGatewayProperties.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/ApiGatewayProperties.java @@ -1,10 +1,14 @@ package com.zt.plat.module.databus.framework.integration.config; +import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * Configuration properties for the unified API portal. @@ -13,24 +17,72 @@ import java.util.List; @ConfigurationProperties(prefix = "databus.api-portal") public class ApiGatewayProperties { - private String basePath = "/api/portal"; + public static final String DEFAULT_BASE_PATH = "/admin-api/databus/api/portal"; + public static final String LEGACY_BASE_PATH = "/databus/api/portal"; + + private String basePath = DEFAULT_BASE_PATH; + + public void setBasePath(String basePath) { + if (!StringUtils.hasText(basePath)) { + this.basePath = DEFAULT_BASE_PATH; + return; + } + String normalized = basePath.startsWith("/") ? basePath : "/" + basePath; + if (LEGACY_BASE_PATH.equals(normalized)) { + this.basePath = DEFAULT_BASE_PATH; + return; + } + this.basePath = normalized; + } + + public List getAllBasePaths() { + Set candidates = new LinkedHashSet<>(); + candidates.add(basePath); + candidates.add(DEFAULT_BASE_PATH); + candidates.add(LEGACY_BASE_PATH); + return new ArrayList<>(candidates); + } private List allowedIps = new ArrayList<>(); private List deniedIps = new ArrayList<>(); - private boolean enableSignature = false; + private Security security = new Security(); - private String signatureHeader = "X-Signature"; + private boolean enableTenantHeader = false; - private String signatureSecret; - - private boolean enableTenantHeader = true; - - private String tenantHeader = "X-Tenant-Id"; + private String tenantHeader = "ZT-Tenant-Id"; private boolean enableAudit = true; private boolean enableRateLimit = true; + @Data + public static class Security { + + private boolean enabled = true; + + private String appIdHeader = "ZT-App-Id"; + + private String timestampHeader = "ZT-Timestamp"; + + private String nonceHeader = "ZT-Nonce"; + + private String signatureHeader = "ZT-Signature"; + + private String signatureType = CryptoSignatureUtils.SIGNATURE_TYPE_MD5; + + private String encryptionType = CryptoSignatureUtils.ENCRYPT_TYPE_AES; + + private long allowedClockSkewSeconds = 300; + + private long nonceTtlSeconds = 600; + + private String nonceRedisKeyPrefix = "databus:gateway:nonce:"; + + private boolean requireBodyEncryption = true; + + private boolean encryptResponse = true; + } + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/GatewayIntegrationConfiguration.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/GatewayIntegrationConfiguration.java index 9d75d547..b92aa746 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/GatewayIntegrationConfiguration.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/GatewayIntegrationConfiguration.java @@ -1,47 +1,30 @@ package com.zt.plat.module.databus.framework.integration.config; -import com.zt.plat.framework.common.exception.ServiceException; -import com.zt.plat.module.databus.framework.integration.gateway.core.ApiFlowDispatcher; -import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayRequestMapper; +import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayExecutionService; import com.zt.plat.module.databus.framework.integration.gateway.core.ErrorHandlingStrategy; -import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; -import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; import com.zt.plat.module.databus.framework.integration.gateway.security.GatewaySecurityFilter; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.integration.core.MessagingTemplate; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.http.dsl.Http; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import org.springframework.util.StringUtils; - -import java.util.HashMap; -import java.util.Map; /** * Configures the unified API portal inbound gateway and supporting beans. */ -@Slf4j @Configuration @EnableConfigurationProperties(ApiGatewayProperties.class) @RequiredArgsConstructor public class GatewayIntegrationConfiguration { private final ApiGatewayProperties properties; - private final ApiGatewayRequestMapper requestMapper; - private final ObjectProvider apiFlowDispatcherProvider; private final ErrorHandlingStrategy errorHandlingStrategy; @Bean(name = "apiPortalTaskExecutor") @@ -68,7 +51,7 @@ public class GatewayIntegrationConfiguration { } @Bean - public IntegrationFlow apiGatewayInboundFlow() { + public IntegrationFlow apiGatewayInboundFlow(ApiGatewayExecutionService executionService) { String pattern = properties.getBasePath() + "/{apiCode}/{version}"; return IntegrationFlow.from(Http.inboundGateway(pattern) .requestMapping(spec -> spec @@ -77,71 +60,9 @@ public class GatewayIntegrationConfiguration { .requestPayloadType(String.class) .mappedRequestHeaders("*") .mappedResponseHeaders("*")) - .handle(this, "mapRequest", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice())) - .handle(this, "dispatch", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice())) - .handle(this, "buildResponse", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice())) + .handle(executionService, "mapRequest", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice())) + .handle(executionService, "dispatch", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice())) + .handle(executionService, "buildResponseEntity", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice())) .get(); } - - public Message mapRequest(Message message) { - ApiInvocationContext context = requestMapper.map(message.getPayload(), message.getHeaders()); - return MessageBuilder.withPayload(context) - .copyHeaders(message.getHeaders()) - .setHeaderIfAbsent("apiCode", context.getApiCode()) - .setHeaderIfAbsent("version", context.getApiVersion()) - .build(); - } - - public ApiInvocationContext dispatch(Message message) { - ApiInvocationContext context = message.getPayload(); - try { - return apiFlowDispatcherProvider.getObject() - .dispatch(context.getApiCode(), context.getApiVersion(), context); - } catch (ServiceException ex) { - handleServiceException(context, ex); - log.warn("[API-PORTAL] ServiceException while dispatching apiCode={} version={}: {}", context.getApiCode(), context.getApiVersion(), ex.getMessage()); - return context; - } catch (Exception ex) { - handleUnexpectedException(context, ex); - log.error("[API-PORTAL] Unexpected exception while dispatching apiCode={} version={}", context.getApiCode(), context.getApiVersion(), ex); - return context; - } - } - - public ResponseEntity buildResponse(ApiInvocationContext context) { - int status = context.getResponseStatus() != null ? context.getResponseStatus() : HttpStatus.OK.value(); - ApiGatewayResponse envelope = ApiGatewayResponse.builder() - .code(status >= 200 && status < 400 ? "SUCCESS" : "ERROR") - .message(StringUtils.hasText(context.getResponseMessage()) ? context.getResponseMessage() : HttpStatus.valueOf(status).getReasonPhrase()) - .data(context.getResponseBody()) - .traceId(context.getRequestId()) - .build(); - return ResponseEntity.status(status).body(envelope); - } - - private void handleServiceException(ApiInvocationContext context, ServiceException ex) { - String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API invocation failed"; - context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - context.setResponseMessage(message); - if (context.getResponseBody() == null) { - Map body = new HashMap<>(); - if (ex.getCode() != null) { - body.put("errorCode", ex.getCode()); - } - body.put("errorMessage", message); - context.setResponseBody(body); - } - } - - private void handleUnexpectedException(ApiInvocationContext context, Exception ex) { - String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API invocation encountered an unexpected error"; - context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - context.setResponseMessage(message); - if (context.getResponseBody() == null) { - Map body = new HashMap<>(); - body.put("errorMessage", message); - body.put("exception", ex.getClass().getSimpleName()); - context.setResponseBody(body); - } - } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowAssembler.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowAssembler.java index a1d8c005..6da2de4c 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowAssembler.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowAssembler.java @@ -9,8 +9,10 @@ import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDe import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor; import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpec; import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpecParser; +import com.zt.plat.module.databus.framework.integration.gateway.expression.GatewayExpressionHelper; import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; import com.zt.plat.module.databus.framework.integration.gateway.step.StepHandlerFactory; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aopalliance.aop.Advice; @@ -27,17 +29,13 @@ import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_PARALLEL_FAILED; -import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_PARALLEL_INTERRUPTED; -import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_TRANSFORM_EVALUATION_FAILED; -import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_TRANSFORM_RESPONSE_STATUS_INVALID; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.*; /** - * Assembles dynamic integration flows per API definition. + * 按 API 定义装配动态集成流程。 */ @Slf4j @Component @@ -58,7 +56,7 @@ public class ApiFlowAssembler { IntegrationFlowBuilder builder = IntegrationFlow.from(MessageChannels.direct(inputChannelName) .datatype(ApiInvocationContext.class) .interceptor(monitoringInterceptor)) - .log(message -> String.format("[API-PORTAL] entering flow %s", flowId)) + .log(message -> String.format("[API-PORTAL] 进入流程 %s", flowId)) .handle(ApiInvocationContext.class, applyTransforms(aggregate, TransformPhaseEnum.REQUEST_PRE), endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice())); @@ -88,6 +86,10 @@ public class ApiFlowAssembler { } private GenericHandler applyTransforms(ApiDefinitionAggregate aggregate, TransformPhaseEnum phase) { + boolean mutateRequest = phase == TransformPhaseEnum.REQUEST_PRE || phase == TransformPhaseEnum.REQUEST_POST; + boolean mutateResponse = phase == TransformPhaseEnum.RESPONSE_PRE + || phase == TransformPhaseEnum.RESPONSE_POST + || phase == TransformPhaseEnum.ERROR; return (payload, headers) -> { var transformDefinition = aggregate.getApiLevelTransforms().get(phase.name()); if (transformDefinition != null && StringUtils.hasText(transformDefinition.getExpression())) { @@ -95,7 +97,7 @@ public class ApiFlowAssembler { ExpressionSpec spec = ExpressionSpecParser.parse(rawExpression, ExpressionTypeEnum.JSON); try { Object result = expressionExecutor.evaluate(spec, payload, payload.getRequestBody(), headers); - applyTransformResult(payload, result); + GatewayExpressionHelper.applyContextMutations(payload, result, mutateRequest, mutateResponse); } catch (Exception ex) { if (ex instanceof ServiceException serviceException) { throw serviceException; @@ -107,48 +109,6 @@ public class ApiFlowAssembler { }; } - private void applyTransformResult(ApiInvocationContext context, Object result) { - if (!(result instanceof Map map)) { - return; - } - Object headerUpdates = map.get("requestHeaders"); - if (headerUpdates instanceof Map headerMap) { - headerMap.forEach((key, value) -> context.getRequestHeaders().put(String.valueOf(key), value)); - } - Object variableUpdates = map.get("variables"); - if (variableUpdates instanceof Map variables) { - variables.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value)); - } - Object attributeUpdates = map.get("attributes"); - if (attributeUpdates instanceof Map attributes) { - attributes.forEach((key, value) -> context.getAttributes().put(String.valueOf(key), value)); - } - if (map.containsKey("responseBody")) { - context.setResponseBody(map.get("responseBody")); - } - if (map.containsKey("responseStatus")) { - context.setResponseStatus(asInteger(map.get("responseStatus"))); - } - if (map.containsKey("responseMessage")) { - Object message = map.get("responseMessage"); - context.setResponseMessage(message == null ? null : String.valueOf(message)); - } - } - - private Integer asInteger(Object value) { - if (value == null) { - return null; - } - if (value instanceof Number number) { - return number.intValue(); - } - try { - return Integer.parseInt(String.valueOf(value)); - } catch (NumberFormatException ex) { - throw ServiceExceptionUtil.exception(API_TRANSFORM_RESPONSE_STATUS_INVALID, value); - } - } - private IntegrationFlowBuilder applySequential(IntegrationFlowBuilder builder, ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) { GenericHandler handler = stepHandlerFactory.build(aggregate, stepDefinition); return builder.handle(ApiInvocationContext.class, handler, endpoint -> { @@ -238,6 +198,7 @@ public class ApiFlowAssembler { private interface FlowSegment { } + @Getter private static final class SequentialSegment implements FlowSegment { private final ApiStepDefinition step; @@ -245,11 +206,9 @@ public class ApiFlowAssembler { this.step = step; } - public ApiStepDefinition getStep() { - return step; - } } + @Getter private static final class ParallelSegment implements FlowSegment { private final String group; private final List steps; @@ -259,12 +218,5 @@ public class ApiFlowAssembler { this.steps = steps; } - public String getGroup() { - return group; - } - - public List getSteps() { - return steps; - } } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowDispatcher.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowDispatcher.java index 2484aaa2..5ff7a99a 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowDispatcher.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowDispatcher.java @@ -13,7 +13,8 @@ import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErro import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_FLOW_NO_REPLY; /** - * Dispatches API invocation contexts to the appropriate integration flow. + * api 分发. + * @author chenbowen */ @Component @RequiredArgsConstructor diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowRegistration.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowRegistration.java index cdd57f1c..1c83f018 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowRegistration.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiFlowRegistration.java @@ -6,6 +6,7 @@ import org.springframework.integration.dsl.IntegrationFlow; /** * Metadata returned by the assembler for flow registration. + * @author chenbowen */ @Value @Builder diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessor.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessor.java new file mode 100644 index 00000000..e7b9dbcb --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayErrorProcessor.java @@ -0,0 +1,97 @@ +package com.zt.plat.module.databus.framework.integration.gateway.core; + +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_AUTH_UNAUTHORIZED; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EXCEEDED; + +/** + * Shared error processor that maps exceptions into {@link ApiInvocationContext} responses. + */ +@Component +public class ApiGatewayErrorProcessor { + + /** + * Applies the given {@link Throwable} to the context, unwrapping {@link ServiceException} + * instances when present and falling back to a generic error payload otherwise. + */ + public void apply(ApiInvocationContext context, Throwable throwable) { + ServiceException serviceException = resolveServiceException(throwable); + if (serviceException != null) { + applyServiceException(context, serviceException); + } else { + applyUnexpectedException(context, throwable); + } + } + + public void applyServiceException(ApiInvocationContext context, ServiceException ex) { + String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API invocation failed"; + context.setResponseStatus(resolveHttpStatus(ex, context)); + context.setResponseMessage(message); + if (context.getResponseBody() == null) { + Map body = new HashMap<>(); + if (ex.getCode() != null) { + body.put("errorCode", ex.getCode()); + } + body.put("errorMessage", message); + context.setResponseBody(body); + } + } + + public void applyUnexpectedException(ApiInvocationContext context, Throwable throwable) { + String message = determineUnexpectedMessage(throwable); + context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + context.setResponseMessage(message); + if (context.getResponseBody() == null) { + Map body = new HashMap<>(); + body.put("errorMessage", message); + body.put("exception", throwable.getClass().getSimpleName()); + context.setResponseBody(body); + } + } + + public ServiceException resolveServiceException(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof ServiceException serviceException) { + return serviceException; + } + current = current.getCause(); + } + return null; + } + + private int resolveHttpStatus(ServiceException ex, ApiInvocationContext context) { + if (context.getResponseStatus() != null) { + return context.getResponseStatus(); + } + Integer code = ex.getCode(); + if (code != null) { + if (API_AUTH_UNAUTHORIZED.getCode().equals(code)) { + return HttpStatus.UNAUTHORIZED.value(); + } + if (API_RATE_LIMIT_EXCEEDED.getCode().equals(code)) { + return HttpStatus.TOO_MANY_REQUESTS.value(); + } + } + return HttpStatus.INTERNAL_SERVER_ERROR.value(); + } + + private String determineUnexpectedMessage(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (StringUtils.hasText(current.getMessage())) { + return current.getMessage(); + } + current = current.getCause(); + } + return "API invocation encountered an unexpected error"; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java new file mode 100644 index 00000000..d84dea8d --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java @@ -0,0 +1,286 @@ +package com.zt.plat.module.databus.framework.integration.gateway.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO; +import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import com.zt.plat.module.databus.framework.integration.gateway.security.GatewayJwtResolver; +import com.zt.plat.module.databus.framework.integration.gateway.security.GatewaySecurityFilter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Array; +import java.net.URI; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Orchestrates API portal request mapping, dispatch and response building so that + * management-side debug invocations and external HTTP requests share identical + * behaviour (other than security concerns handled by {@link GatewaySecurityFilter}). + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ApiGatewayExecutionService { + + private static final String HEADER_REQUEST_HEADERS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders"; + private static final String HEADER_REQUEST_URI = org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri"; + private static final String HEADER_REQUEST_PARAMS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestParams"; + private static final String HEADER_QUERY_STRING = org.springframework.integration.http.HttpHeaders.PREFIX + "queryString"; + + private final ApiGatewayRequestMapper requestMapper; + private final ApiFlowDispatcher apiFlowDispatcher; + private final ApiGatewayErrorProcessor errorProcessor; + private final ApiGatewayProperties properties; + private final ObjectMapper objectMapper; + + /** + * Maps a raw HTTP message (as provided by Spring Integration) into a context message. + */ + public Message mapRequest(Message message) { + ApiInvocationContext context = requestMapper.map(message.getPayload(), message.getHeaders()); + return MessageBuilder.withPayload(context) + .copyHeaders(message.getHeaders()) + .setHeaderIfAbsent("apiCode", context.getApiCode()) + .setHeaderIfAbsent("version", context.getApiVersion()) + .build(); + } + + /** + * Dispatches the API invocation and applies gateway error processing rules on failure scenarios. + */ + public ApiInvocationContext dispatch(Message message) { + ApiInvocationContext context = message.getPayload(); + try { + return apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context); + } catch (ServiceException ex) { + errorProcessor.applyServiceException(context, ex); + log.warn("[API-PORTAL] 分发 apiCode={} version={} 时出现 ServiceException: {}", context.getApiCode(), context.getApiVersion(), ex.getMessage()); + return context; + } catch (Exception ex) { + ServiceException nestedServiceException = errorProcessor.resolveServiceException(ex); + if (nestedServiceException != null) { + errorProcessor.applyServiceException(context, nestedServiceException); + log.warn("[API-PORTAL] 分发 apiCode={} version={} 时出现 ServiceException(包装异常): {}", context.getApiCode(), context.getApiVersion(), nestedServiceException.getMessage()); + if (log.isDebugEnabled()) { + log.debug("[API-PORTAL] 包装异常堆栈", ex); + } + } else { + errorProcessor.applyUnexpectedException(context, ex); + log.error("[API-PORTAL] 分发 apiCode={} version={} 时出现未预期异常", context.getApiCode(), context.getApiVersion(), ex); + } + return context; + } + } + + /** + * Builds a HTTP response entity for the external gateway flow. + */ + public ResponseEntity buildResponseEntity(ApiInvocationContext context) { + int status = resolveStatus(context); + ApiGatewayResponse envelope = buildResponseEnvelope(context, status); + return ResponseEntity.status(status).body(envelope); + } + + /** + * Executes a debug invocation by reusing the same mapping/dispatch pipeline as the public gateway. + */ + public ResponseEntity invokeForDebug(ApiGatewayInvokeReqVO reqVO) { + Message rawMessage = buildDebugMessage(reqVO); + Message mappedMessage = mapRequest(rawMessage); + ApiInvocationContext context = mappedMessage.getPayload(); + // Ensure query parameters & headers from debug payload are reflected after mapping. + mergeDebugMetadata(context, reqVO); + ApiInvocationContext responseContext = dispatch(mappedMessage); + return buildResponseEntity(responseContext); + } + + private Message buildDebugMessage(ApiGatewayInvokeReqVO reqVO) { + Object payload = preparePayload(reqVO.getPayload()); + MessageBuilder builder = MessageBuilder.withPayload(payload); + + Map uriVariables = Map.of( + "apiCode", reqVO.getApiCode(), + "version", reqVO.getVersion() + ); + builder.setHeader(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables); + builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, HttpMethod.POST.name()); + + String basePath = normalizeBasePath(properties.getBasePath()); + String rawQuery = buildQueryString(reqVO.getQueryParams()); + String requestUri = basePath + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion(); + if (StringUtils.hasText(rawQuery)) { + requestUri = requestUri + "?" + rawQuery; + } + builder.setHeader(HEADER_REQUEST_URI, requestUri); + builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_URL, requestUri); + + Map requestHeaders = new LinkedHashMap<>(); + if (reqVO.getHeaders() != null) { + reqVO.getHeaders().forEach(requestHeaders::put); + } + normalizeJwtHeaders(requestHeaders, reqVO.getQueryParams()); + requestHeaders.putIfAbsent(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + builder.setHeader(HEADER_REQUEST_HEADERS, requestHeaders); + requestHeaders.forEach((key, value) -> { + if (value != null) { + builder.setHeaderIfAbsent(key, value); + } + }); + + if (reqVO.getQueryParams() != null && !reqVO.getQueryParams().isEmpty()) { + Map paramsCopy = new LinkedHashMap<>(reqVO.getQueryParams()); + builder.setHeader(HEADER_REQUEST_PARAMS, paramsCopy); + if (StringUtils.hasText(rawQuery)) { + builder.setHeader(HEADER_QUERY_STRING, rawQuery); + } + } + + if (properties.isEnableTenantHeader()) { + String tenantHeader = properties.getTenantHeader(); + Object tenantValue = requestHeaders.get(tenantHeader); + if (tenantValue != null) { + builder.setHeaderIfAbsent(tenantHeader, tenantValue); + } + } + + return builder.build(); + } + + private Object preparePayload(Object payload) { + if (payload == null) { + return ""; + } + if (payload instanceof String) { + return payload; + } + try { + return objectMapper.writeValueAsString(payload); + } catch (JsonProcessingException ex) { + log.debug("[API-PORTAL] 将调试请求体序列化为 JSON 失败,使用 toString()", ex); + return payload.toString(); + } + } + + private void mergeDebugMetadata(ApiInvocationContext context, ApiGatewayInvokeReqVO reqVO) { + if (reqVO.getHeaders() != null && !reqVO.getHeaders().isEmpty()) { + reqVO.getHeaders().forEach((key, value) -> context.getRequestHeaders().putIfAbsent(key, value)); + } + if (reqVO.getQueryParams() != null && !reqVO.getQueryParams().isEmpty()) { + reqVO.getQueryParams().forEach((key, value) -> context.getRequestQueryParams().putIfAbsent(key, value)); + } + if (!StringUtils.hasText(context.getHttpMethod())) { + context.setHttpMethod(HttpMethod.POST.name()); + } + if (!StringUtils.hasText(context.getRequestPath())) { + String basePath = normalizeBasePath(properties.getBasePath()); + String path = basePath + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion(); + context.setRequestPath(path); + } + } + + private int resolveStatus(ApiInvocationContext context) { + return context.getResponseStatus() != null ? context.getResponseStatus() : HttpStatus.OK.value(); + } + + private ApiGatewayResponse buildResponseEnvelope(ApiInvocationContext context, int status) { + String message = StringUtils.hasText(context.getResponseMessage()) + ? context.getResponseMessage() + : HttpStatus.valueOf(status).getReasonPhrase(); + return ApiGatewayResponse.builder() + .code(status) + .message(message) + .response(context.getResponseBody()) + .traceId(context.getRequestId()) + .build(); + } + + private String normalizeBasePath(String basePath) { + if (!StringUtils.hasText(basePath)) { + return ApiGatewayProperties.DEFAULT_BASE_PATH; + } + String normalized = basePath.startsWith("/") ? basePath : "/" + basePath; + while (normalized.endsWith("/") && normalized.length() > 1) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized; + } + + private String buildQueryString(Map queryParams) { + if (queryParams == null || queryParams.isEmpty()) { + return null; + } + UriComponentsBuilder builder = UriComponentsBuilder.newInstance(); + queryParams.forEach((key, value) -> appendQueryParam(builder, key, value)); + URI uri = builder.build(true).toUri(); + return uri.getQuery(); + } + + private void appendQueryParam(UriComponentsBuilder builder, String key, Object value) { + if (!StringUtils.hasText(key)) { + return; + } + if (value == null) { + builder.queryParam(key, (Object) null); + return; + } + if (value instanceof Iterable iterable) { + for (Object element : iterable) { + appendQueryParam(builder, key, element); + } + return; + } + if (value.getClass().isArray()) { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + appendQueryParam(builder, key, Array.get(value, i)); + } + return; + } + builder.queryParam(key, value); + } + + private void normalizeJwtHeaders(Map headers, Map queryParams) { + String token = GatewayJwtResolver.resolveJwtToken(headers, queryParams, objectMapper); + if (!StringUtils.hasText(token)) { + return; + } + ensureHeaderValue(headers, GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, token); + ensureHeaderValue(headers, HttpHeaders.AUTHORIZATION, "Bearer " + token); + } + + private void ensureHeaderValue(Map headers, String headerName, String value) { + if (!StringUtils.hasText(headerName) || value == null) { + return; + } + String existingKey = findHeaderKey(headers, headerName); + if (existingKey != null) { + headers.put(existingKey, value); + } else { + headers.put(headerName, value); + } + } + + private String findHeaderKey(Map headers, String headerName) { + if (headers == null || !StringUtils.hasText(headerName)) { + return null; + } + for (String key : headers.keySet()) { + if (headerName.equalsIgnoreCase(key)) { + return key; + } + } + return null; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java index c050f260..efce57c7 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java @@ -8,10 +8,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -34,8 +39,14 @@ public class ApiGatewayRequestMapper { ApiInvocationContext context = ApiInvocationContext.create(); Map uriVariables = (Map) headers.get(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); if (uriVariables != null) { - context.setApiCode(String.valueOf(uriVariables.get("apiCode"))); - context.setApiVersion(String.valueOf(uriVariables.get("version"))); + Object apiCode = uriVariables.get("apiCode"); + Object version = uriVariables.get("version"); + if (apiCode != null) { + context.setApiCode(String.valueOf(apiCode)); + } + if (version != null) { + context.setApiVersion(String.valueOf(version)); + } } Object methodHeader = headers.get(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD); if (methodHeader != null) { @@ -45,13 +56,38 @@ public class ApiGatewayRequestMapper { if (requestPath == null) { requestPath = headers.get(org.springframework.integration.http.HttpHeaders.REQUEST_URL); } - if (requestPath != null) { - context.setRequestPath(String.valueOf(requestPath)); + String originalRequestUri = requestPath != null ? String.valueOf(requestPath) : null; + if (originalRequestUri != null) { + context.setRequestPath(originalRequestUri); } + if (!StringUtils.hasText(context.getApiCode())) { + Object apiCodeHeader = headers.get("apiCode"); + if (apiCodeHeader != null) { + context.setApiCode(String.valueOf(apiCodeHeader)); + } + } + if (!StringUtils.hasText(context.getApiVersion())) { + Object versionHeader = headers.get("version"); + if (versionHeader != null) { + context.setApiVersion(String.valueOf(versionHeader)); + } + } + if (!StringUtils.hasText(context.getApiCode()) || !StringUtils.hasText(context.getApiVersion())) { + inferFromRequestPath(context); + } + Map requestHeaders = (Map) headers.get(HEADER_REQUEST_HEADERS); - if (requestHeaders != null) { - requestHeaders.forEach((key, value) -> context.getRequestHeaders().put(key, String.valueOf(value))); - } + GatewayHeaderUtils.mergeNormalizedHeaders(requestHeaders, context.getRequestHeaders()); + headers.forEach((key, value) -> { + if (isInternalHeader(key)) { + return; + } + String normalized = GatewayHeaderUtils.normalizeHeaderValue(value); + if (normalized != null) { + context.getRequestHeaders().putIfAbsent(key, normalized); + } + }); + populateQueryParams(headers, context, originalRequestUri); if (properties.isEnableTenantHeader()) { Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader()); if (tenantHeaderValue != null) { @@ -63,7 +99,7 @@ public class ApiGatewayRequestMapper { try { context.setRequestBody(objectMapper.readValue(body, Object.class)); } catch (IOException ex) { - log.warn("Failed to parse request body as JSON", ex); + log.warn("解析请求体为 JSON 失败", ex); context.setRequestBody(body); } } else { @@ -75,8 +111,208 @@ public class ApiGatewayRequestMapper { return context; } + private boolean isInternalHeader(String headerName) { + if (!StringUtils.hasText(headerName)) { + return true; + } + if (headerName.startsWith(org.springframework.integration.http.HttpHeaders.PREFIX)) { + return true; + } + if (org.springframework.messaging.MessageHeaders.ID.equals(headerName) + || org.springframework.messaging.MessageHeaders.TIMESTAMP.equals(headerName)) { + return true; + } + return "correlationId".equals(headerName) + || "sequenceNumber".equals(headerName) + || "sequenceSize".equals(headerName) + || "errorChannel".equals(headerName) + || "replyChannel".equals(headerName) + || "replyChannelName".equals(headerName) + || "errorChannelName".equals(headerName); + } + private boolean isJsonContent(ApiInvocationContext context) { String contentType = String.valueOf(context.getRequestHeaders().getOrDefault(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)).toLowerCase(Locale.ROOT); return contentType.contains(MediaType.APPLICATION_JSON_VALUE); } + + private void populateQueryParams(Map headers, ApiInvocationContext context, String originalRequestUri) { + Map headerParams = findRequestParamsHeader(headers); + if (headerParams != null) { + mergeParameterMap(headerParams, context.getRequestQueryParams(), true); + } + + String queryString = findQueryStringHeader(headers); + if (!StringUtils.hasText(queryString) && StringUtils.hasText(originalRequestUri)) { + int idx = originalRequestUri.indexOf('?'); + if (idx >= 0 && idx + 1 < originalRequestUri.length()) { + queryString = originalRequestUri.substring(idx + 1); + } + } + if (StringUtils.hasText(queryString)) { + mergeQueryString(queryString, context.getRequestQueryParams()); + } + } + + private Map findRequestParamsHeader(Map headers) { + if (headers == null) { + return null; + } + for (Map.Entry entry : headers.entrySet()) { + String key = entry.getKey(); + if (!StringUtils.hasText(key)) { + continue; + } + if (entry.getValue() instanceof Map map && key.toLowerCase(Locale.ROOT).endsWith("requestparams")) { + return map; + } + } + return null; + } + + private String findQueryStringHeader(Map headers) { + if (headers == null) { + return null; + } + for (Map.Entry entry : headers.entrySet()) { + String key = entry.getKey(); + if (!StringUtils.hasText(key)) { + continue; + } + if (key.toLowerCase(Locale.ROOT).endsWith("querystring") && entry.getValue() != null) { + return entry.getValue().toString(); + } + } + return null; + } + + private void mergeQueryString(String queryString, Map target) { + try { + MultiValueMap params = UriComponentsBuilder.newInstance() + .query(queryString) + .build() + .getQueryParams(); + params.forEach((key, values) -> { + if (!StringUtils.hasText(key) || values == null || values.isEmpty()) { + return; + } + if (target.containsKey(key)) { + return; + } + if (values.size() == 1) { + target.put(key, values.get(0)); + } else { + target.put(key, List.copyOf(values)); + } + }); + } catch (IllegalArgumentException ex) { + log.debug("解析查询串 '{}' 失败", queryString, ex); + } + } + + private void mergeParameterMap(Map source, Map target, boolean override) { + source.forEach((rawKey, rawValue) -> { + if (!(rawKey instanceof String key) || !StringUtils.hasText(key)) { + return; + } + List values = normalizeParamValues(rawValue); + if (values.isEmpty()) { + return; + } + if (!override && target.containsKey(key)) { + return; + } + if (values.size() == 1) { + target.put(key, values.get(0)); + } else { + target.put(key, values); + } + }); + } + + private List normalizeParamValues(Object value) { + if (value == null) { + return List.of(); + } + if (value instanceof CharSequence) { + String candidate = value.toString(); + return candidate.isEmpty() ? List.of("") : List.of(candidate); + } + if (value instanceof Iterable) { + List result = new ArrayList<>(); + for (Object element : (Iterable) value) { + result.addAll(normalizeParamValues(element)); + } + return result; + } + if (value.getClass().isArray()) { + int length = Array.getLength(value); + List result = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + result.addAll(normalizeParamValues(Array.get(value, i))); + } + return result; + } + return List.of(value.toString()); + } + + private void inferFromRequestPath(ApiInvocationContext context) { + String requestPath = normalizeRequestPath(context.getRequestPath()); + if (!StringUtils.hasText(requestPath)) { + return; + } + if (!requestPath.equals(context.getRequestPath())) { + context.setRequestPath(requestPath); + } + for (String basePath : properties.getAllBasePaths()) { + if (!requestPath.startsWith(basePath)) { + continue; + } + String remainder = requestPath.substring(basePath.length()); + if (remainder.startsWith("/")) { + remainder = remainder.substring(1); + } + if (!StringUtils.hasText(remainder)) { + continue; + } + String[] segments = remainder.split("/"); + if (segments.length < 2) { + continue; + } + if (!StringUtils.hasText(context.getApiCode())) { + context.setApiCode(segments[0]); + } + if (!StringUtils.hasText(context.getApiVersion())) { + context.setApiVersion(segments[1]); + } + if (StringUtils.hasText(context.getApiCode()) && StringUtils.hasText(context.getApiVersion())) { + return; + } + } + } + + private String normalizeRequestPath(String requestPath) { + if (!StringUtils.hasText(requestPath)) { + return requestPath; + } + String candidate = requestPath; + int queryIndex = candidate.indexOf('?'); + if (queryIndex >= 0) { + candidate = candidate.substring(0, queryIndex); + } + if (candidate.contains("://")) { + try { + java.net.URI uri = java.net.URI.create(candidate); + if (uri.getPath() != null) { + candidate = uri.getPath(); + } + } catch (IllegalArgumentException ex) { + log.debug("将请求路径 '{}' 解析为 URI 失败", candidate, ex); + } + } + if (!candidate.startsWith("/")) { + candidate = "/" + candidate; + } + return candidate; + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ErrorHandlingStrategy.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ErrorHandlingStrategy.java index e7307110..ac9cad2d 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ErrorHandlingStrategy.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ErrorHandlingStrategy.java @@ -14,17 +14,19 @@ import org.springframework.messaging.support.ErrorMessage; import org.springframework.stereotype.Component; /** - * Centralized error channel and handler for the API portal. + * 为 API 门户集中管理错误通道与处理器。 */ @Slf4j @Component public class ErrorHandlingStrategy { + private final ApiGatewayErrorProcessor errorProcessor; @Getter private final MessageChannel errorChannel; private final Advice errorForwardingAdvice; - public ErrorHandlingStrategy() { + public ErrorHandlingStrategy(ApiGatewayErrorProcessor errorProcessor) { + this.errorProcessor = errorProcessor; DirectChannel channel = MessageChannels.direct("apiPortalErrorChannel").getObject(); this.errorChannel = channel; channel.subscribe(this::handleErrorMessage); @@ -45,10 +47,9 @@ public class ErrorHandlingStrategy { Throwable throwable = errorMessage.getPayload(); Message failedMessage = errorMessage.getOriginalMessage(); if (failedMessage != null && failedMessage.getPayload() instanceof ApiInvocationContext context) { - context.setResponseStatus(500); - context.setResponseMessage(throwable.getMessage()); + errorProcessor.apply(context, throwable); } - log.error("[API-PORTAL] Integration flow error", throwable); + log.error("[API-PORTAL] 集成流程发生错误", throwable); } private class ErrorForwardingAdvice extends AbstractHandleMessageAdvice { @@ -61,10 +62,10 @@ public class ErrorHandlingStrategy { ErrorMessage errorMessage = new ErrorMessage(ex, message); try { if (!errorChannel.send(errorMessage)) { - log.warn("[API-PORTAL] Failed to forward error message to channel {}", errorChannel); + log.warn("[API-PORTAL] 无法将错误消息转发到通道 {}", errorChannel); } } catch (Exception sendEx) { - log.error("[API-PORTAL] Error while submitting message to error channel", sendEx); + log.error("[API-PORTAL] 向错误通道投递消息时出错", sendEx); } throw ex; } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/GatewayHeaderUtils.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/GatewayHeaderUtils.java new file mode 100644 index 00000000..04d3677c --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/GatewayHeaderUtils.java @@ -0,0 +1,81 @@ +package com.zt.plat.module.databus.framework.integration.gateway.core; + +import org.springframework.util.StringUtils; + +import java.lang.reflect.Array; +import java.util.Map; + +/** + * Utility helpers for working with request headers inside the gateway module. + */ +public final class GatewayHeaderUtils { + + private GatewayHeaderUtils() { + } + + public static void mergeNormalizedHeaders(Map source, Map target) { + if (source == null || target == null) { + return; + } + source.forEach((key, value) -> { + String normalized = normalizeHeaderValue(value); + if (normalized != null) { + target.put(key, normalized); + } + }); + } + + public static String findFirstHeaderValue(Map headers, String headerName) { + if (headers == null || !StringUtils.hasText(headerName)) { + return null; + } + for (Map.Entry entry : headers.entrySet()) { + if (headerName.equalsIgnoreCase(entry.getKey())) { + return normalizeHeaderValue(entry.getValue()); + } + } + return null; + } + + public static String stripBearerPrefix(String token) { + if (!StringUtils.hasText(token)) { + return token; + } + String candidate = token.trim(); + if (candidate.regionMatches(true, 0, "Bearer ", 0, 7)) { + candidate = candidate.substring(7).trim(); + } + return candidate; + } + + static String normalizeHeaderValue(Object value) { + if (value == null) { + return null; + } + if (value instanceof CharSequence sequence) { + String candidate = sequence.toString().trim(); + return StringUtils.hasText(candidate) ? candidate : null; + } + if (value instanceof Iterable iterable) { + for (Object element : iterable) { + String candidate = normalizeHeaderValue(element); + if (candidate != null) { + return candidate; + } + } + return null; + } + if (value.getClass().isArray()) { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + String candidate = normalizeHeaderValue(Array.get(value, i)); + if (candidate != null) { + return candidate; + } + } + return null; + } + String candidate = value.toString().trim(); + return StringUtils.hasText(candidate) ? candidate : null; + } +} \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/IntegrationFlowManager.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/IntegrationFlowManager.java index 0867b24b..bcd985ae 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/IntegrationFlowManager.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/IntegrationFlowManager.java @@ -15,7 +15,7 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; /** - * Manages dynamic registration of API integration flows. + * 管理 API 集成流程的动态注册。 */ @Slf4j @Component @@ -71,7 +71,7 @@ public class IntegrationFlowManager { .id(apiFlowRegistration.getFlowId()) .register(); activeRegistrations.put(key, registration); - log.info("[API-PORTAL] registered flow {} for apiCode={} version={}", apiFlowRegistration.getFlowId(), aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion()); + log.info("[API-PORTAL] 已注册流程 {} 对应 apiCode={} version={}", apiFlowRegistration.getFlowId(), aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion()); } private void deregister(String apiCode, String version) { @@ -83,9 +83,9 @@ public class IntegrationFlowManager { if (existing != null) { try { integrationFlowContext.remove(existing.getId()); - log.info("[API-PORTAL] deregistered flow {} for key {}", existing.getId(), key); + log.info("[API-PORTAL] 已注销流程 {} 对应 key {}", existing.getId(), key); } catch (Exception ex) { - log.warn("Failed to remove integration flow {}", existing.getId(), ex); + log.warn("移除集成流程 {} 失败", existing.getId(), ex); } } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/MonitoringInterceptor.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/MonitoringInterceptor.java index f2895a99..0ba281a2 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/MonitoringInterceptor.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/MonitoringInterceptor.java @@ -15,7 +15,7 @@ import java.time.Duration; import java.time.Instant; /** - * Channel interceptor capturing timing metrics and enriched logging. + * 通道拦截器,用于捕获耗时指标并增强日志记录。 */ @Slf4j @Component @@ -48,7 +48,7 @@ public class MonitoringInterceptor implements ChannelInterceptor { } } if (ex != null) { - log.error("[API-PORTAL] Channel send failed", ex); + log.error("[API-PORTAL] 通道发送失败", ex); } } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/PolicyAdvisorFactory.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/PolicyAdvisorFactory.java index 37360507..30937359 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/PolicyAdvisorFactory.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/PolicyAdvisorFactory.java @@ -5,19 +5,14 @@ import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; -import com.zt.plat.module.databus.framework.integration.gateway.policy.AuthPolicyEvaluator; import com.zt.plat.module.databus.framework.integration.gateway.policy.RateLimitPolicyEvaluator; import lombok.RequiredArgsConstructor; +import org.aopalliance.aop.Advice; import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice; -import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice; -import org.springframework.retry.backoff.ExponentialBackOffPolicy; -import org.springframework.retry.policy.SimpleRetryPolicy; -import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; -import java.util.Map; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_EXECUTION_ERROR; @@ -28,74 +23,21 @@ import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErro @RequiredArgsConstructor public class PolicyAdvisorFactory { - private final AuthPolicyEvaluator authPolicyEvaluator; private final RateLimitPolicyEvaluator rateLimitPolicyEvaluator; - public org.aopalliance.aop.Advice[] buildAdvices(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) { - List advices = new ArrayList<>(); - advices.add(new AuthPolicyAdvice(aggregate)); - advices.add(new RateLimitPolicyAdvice(aggregate)); - advices.add(createRetryAdvice(stepDefinition)); - return advices.stream().filter(advice -> advice != null).toArray(org.aopalliance.aop.Advice[]::new); + public Advice[] buildAdvices(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) { + List advices = new ArrayList<>(); + if (aggregate.getRateLimitPolicy() != null) { + advices.add(new RateLimitPolicyAdvice(aggregate)); + } + return advices.toArray(Advice[]::new); } - public org.aopalliance.aop.Advice[] buildParallelAdvices(ApiDefinitionAggregate aggregate, Object segment) { - // For parallel segments we reuse the same advice chain (auth + rateLimit once at entry) + public Advice[] buildParallelAdvices(ApiDefinitionAggregate aggregate, Object segment) { + // For parallel segments we reuse the same rate-limit advice chain return buildAdvices(aggregate, null); } - private RequestHandlerRetryAdvice createRetryAdvice(ApiStepDefinition stepDefinition) { - if (stepDefinition == null) { - return null; - } - Object strategyConfig = stepDefinition.getMetadata().get("retryStrategy"); - if (!(strategyConfig instanceof Map configMap)) { - return null; - } - RetryTemplate template = new RetryTemplate(); - SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); - int maxAttempts = asInt(configMap.get("maxAttempts"), 3); - retryPolicy.setMaxAttempts(maxAttempts); - ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); - long initialInterval = asLong(configMap.get("initialInterval"), 200L); - double multiplier = asDouble(configMap.get("multiplier"), 2.0d); - long maxInterval = asLong(configMap.get("maxInterval"), 2000L); - backOffPolicy.setInitialInterval(initialInterval); - backOffPolicy.setMultiplier(multiplier); - backOffPolicy.setMaxInterval(maxInterval); - template.setBackOffPolicy(backOffPolicy); - template.setRetryPolicy(retryPolicy); - RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice(); - advice.setRetryTemplate(template); - return advice; - } - - private final class AuthPolicyAdvice extends AbstractRequestHandlerAdvice { - private final ApiDefinitionAggregate aggregate; - - private AuthPolicyAdvice(ApiDefinitionAggregate aggregate) { - this.aggregate = aggregate; - } - - @Override - protected Object doInvoke(ExecutionCallback callback, Object target, org.springframework.messaging.Message message) { - if (aggregate.getAuthPolicy() != null) { - authPolicyEvaluator.evaluate(aggregate, (ApiInvocationContext) message.getPayload()); - } - try { - return callback.execute(); - } catch (Exception ex) { - if (ex instanceof ServiceException serviceException) { - throw serviceException; - } - if (ex instanceof RuntimeException runtimeException) { - throw runtimeException; - } - throw ServiceExceptionUtil.exception(API_STEP_EXECUTION_ERROR, ex.getMessage()); - } - } - } - private final class RateLimitPolicyAdvice extends AbstractRequestHandlerAdvice { private final ApiDefinitionAggregate aggregate; @@ -121,46 +63,4 @@ public class PolicyAdvisorFactory { } } } - - private int asInt(Object value, int defaultValue) { - if (value instanceof Number number) { - return number.intValue(); - } - if (value instanceof String text) { - try { - return Integer.parseInt(text); - } catch (NumberFormatException ignored) { - // ignore and fall back to default - } - } - return defaultValue; - } - - private long asLong(Object value, long defaultValue) { - if (value instanceof Number number) { - return number.longValue(); - } - if (value instanceof String text) { - try { - return Long.parseLong(text); - } catch (NumberFormatException ignored) { - // ignore and fall back to default - } - } - return defaultValue; - } - - private double asDouble(Object value, double defaultValue) { - if (value instanceof Number number) { - return number.doubleValue(); - } - if (value instanceof String text) { - try { - return Double.parseDouble(text); - } catch (NumberFormatException ignored) { - // ignore and fall back to default - } - } - return defaultValue; - } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java index 34a5bdef..24121989 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java @@ -1,7 +1,6 @@ package com.zt.plat.module.databus.framework.integration.gateway.domain; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO; import lombok.Builder; import lombok.Value; @@ -23,8 +22,6 @@ public class ApiDefinitionAggregate { Map apiLevelTransforms; - ApiPolicyAuthDO authPolicy; - ApiPolicyRateLimitDO rateLimitPolicy; ApiFlowPublication publication; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/GatewayExpressionHelper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/GatewayExpressionHelper.java new file mode 100644 index 00000000..425357f5 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/GatewayExpressionHelper.java @@ -0,0 +1,168 @@ +package com.zt.plat.module.databus.framework.integration.gateway.expression; + +import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_TRANSFORM_RESPONSE_STATUS_INVALID; + +/** + * Utility helpers for mutating {@link ApiInvocationContext} based on expression evaluation results. + */ +public final class GatewayExpressionHelper { + + private GatewayExpressionHelper() { + } + + public static void applyContextMutations(ApiInvocationContext context, Object result, + boolean allowRequestMutations, boolean allowResponseMutations) { + if (result == null) { + return; + } + if (result instanceof Map map) { + applyCommonMutations(context, map); + if (allowRequestMutations) { + applyRequestMutations(context, map); + } + if (allowResponseMutations) { + applyResponseMutations(context, map); + } + return; + } + if (allowRequestMutations) { + context.setRequestBody(result); + } else if (allowResponseMutations) { + context.setResponseBody(result); + } + } + + public static Map snapshotRequest(ApiInvocationContext context) { + Map snapshot = new LinkedHashMap<>(); + if (context.getRequestBody() != null) { + snapshot.put("body", context.getRequestBody()); + } + if (context.getRequestQueryParams() != null && !context.getRequestQueryParams().isEmpty()) { + snapshot.put("query", new LinkedHashMap<>(context.getRequestQueryParams())); + } + if (context.getRequestHeaders() != null && !context.getRequestHeaders().isEmpty()) { + snapshot.put("headers", new LinkedHashMap<>(context.getRequestHeaders())); + } + if (snapshot.isEmpty()) { + return null; + } + return snapshot; + } + + public static Map snapshotResponse(ApiInvocationContext context) { + Map snapshot = new LinkedHashMap<>(); + if (context.getResponseBody() != null) { + snapshot.put("body", context.getResponseBody()); + } + if (context.getResponseStatus() != null) { + snapshot.put("status", context.getResponseStatus()); + } + if (context.getResponseMessage() != null) { + snapshot.put("message", context.getResponseMessage()); + } + if (snapshot.isEmpty()) { + return null; + } + return snapshot; + } + + private static void applyCommonMutations(ApiInvocationContext context, Map map) { + Object variables = map.get("variables"); + if (variables instanceof Map variableMap) { + context.getVariables().putAll(toObjectMap(variableMap)); + } + Object attributes = map.get("attributes"); + if (attributes instanceof Map attributeMap) { + context.getAttributes().putAll(toObjectMap(attributeMap)); + } + Object tenantId = map.get("tenantId"); + if (tenantId != null) { + context.setTenantId(String.valueOf(tenantId)); + } + Object httpMethod = map.get("httpMethod"); + if (httpMethod != null) { + context.setHttpMethod(String.valueOf(httpMethod)); + } + Object requestPath = map.get("requestPath"); + if (requestPath != null) { + context.setRequestPath(String.valueOf(requestPath)); + } + } + + private static void applyRequestMutations(ApiInvocationContext context, Map map) { + Object body = firstNonNull(map.get("requestBody"), map.get("body")); + if (body != null) { + context.setRequestBody(body); + } + Object headers = map.get("requestHeaders"); + if (headers instanceof Map headerMap) { + context.getRequestHeaders().putAll(toStringMap(headerMap)); + } + Object query = firstNonNull(map.get("requestQuery"), map.get("requestQueryParams"), map.get("query")); + if (query instanceof Map queryMap) { + context.getRequestQueryParams().putAll(toObjectMap(queryMap)); + } + } + + private static void applyResponseMutations(ApiInvocationContext context, Map map) { + Object body = firstNonNull(map.get("responseBody"), map.get("body")); + if (body != null) { + context.setResponseBody(body); + } + Object status = map.get("responseStatus"); + if (status != null) { + context.setResponseStatus(asInteger(status)); + } + Object message = map.get("responseMessage"); + if (message != null) { + context.setResponseMessage(String.valueOf(message)); + } + } + + private static Map toObjectMap(Map origin) { + Map result = new LinkedHashMap<>(); + origin.forEach((key, value) -> result.put(String.valueOf(key), value)); + return result; + } + + private static Map toStringMap(Map origin) { + Map result = new LinkedHashMap<>(); + origin.forEach((key, value) -> { + if (value != null) { + result.put(String.valueOf(key), String.valueOf(value)); + } + }); + return result; + } + + private static Integer asInteger(Object value) { + if (value == null) { + return null; + } + if (value instanceof Number number) { + return number.intValue(); + } + String raw = String.valueOf(value); + try { + return Integer.parseInt(raw); + } catch (NumberFormatException ex) { + throw ServiceExceptionUtil.exception(API_TRANSFORM_RESPONSE_STATUS_INVALID, raw); + } + } + + @SafeVarargs + private static T firstNonNull(T... values) { + for (T value : values) { + if (value != null) { + return value; + } + } + return null; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/JsScriptExpressionEvaluator.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/JsScriptExpressionEvaluator.java deleted file mode 100644 index 42c8a812..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/JsScriptExpressionEvaluator.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.zt.plat.module.databus.framework.integration.gateway.expression; - -/** - * Legacy placeholder kept for binary compatibility. JSR-223 scripts are no longer supported. - */ -@Deprecated(forRemoval = true) -public class JsScriptExpressionEvaluator implements ExpressionEvaluator { - - @Override - public Object evaluate(String expression, ExpressionEvaluationContext context) { - throw new UnsupportedOperationException("JSR-223 script expressions are no longer supported. Use JSON expressions instead."); - } -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/MvelExpressionEvaluator.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/MvelExpressionEvaluator.java deleted file mode 100644 index 443526f4..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/MvelExpressionEvaluator.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.zt.plat.module.databus.framework.integration.gateway.expression; - -/** - * Legacy placeholder kept for binary compatibility. MVEL evaluation is no longer supported. - */ -@Deprecated(forRemoval = true) -public class MvelExpressionEvaluator implements ExpressionEvaluator { - - @Override - public Object evaluate(String expression, ExpressionEvaluationContext context) { - throw new UnsupportedOperationException("MVEL expressions are no longer supported. Use JSON expressions instead."); - } -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/SpelExpressionEvaluator.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/SpelExpressionEvaluator.java deleted file mode 100644 index c6987f40..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/SpelExpressionEvaluator.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.zt.plat.module.databus.framework.integration.gateway.expression; - -/** - * Legacy placeholder kept for binary compatibility. SpEL evaluation is no longer supported. - */ -@Deprecated(forRemoval = true) -public class SpelExpressionEvaluator implements ExpressionEvaluator { - - @Override - public Object evaluate(String expression, ExpressionEvaluationContext context) { - throw new UnsupportedOperationException("SpEL expressions are no longer supported. Use JSON expressions instead."); - } -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/init/GatewayPolicyMigration.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/init/GatewayPolicyMigration.java index f7c95b6e..69f8724f 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/init/GatewayPolicyMigration.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/init/GatewayPolicyMigration.java @@ -5,8 +5,7 @@ import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; /** - * Applies idempotent data adjustments required for gateway orchestration features - * before integration flows bootstrap. + * 在集成流程启动前执行网关编排所需的幂等数据调整。 */ @Slf4j @Component("gatewayPolicyMigration") @@ -14,6 +13,6 @@ public class GatewayPolicyMigration { @PostConstruct public void migrate() { - log.info("[API-PORTAL] gateway policy migration skipped; standard header token auth in use"); + log.info("[API-PORTAL] 跳过网关策略迁移,继续使用标准头部令牌认证"); } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiGatewayResponse.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiGatewayResponse.java index 3fe9455d..a1d3afb0 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiGatewayResponse.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiGatewayResponse.java @@ -10,11 +10,14 @@ import lombok.Value; @Builder public class ApiGatewayResponse { - String code; + /** + * HTTP status code returned by the gateway. Always aligns with the response status line. + */ + int code; String message; - Object data; + Object response; String traceId; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java index 3f1a6dd3..dc1840bb 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/model/ApiInvocationContext.java @@ -5,11 +5,7 @@ import lombok.Setter; import lombok.ToString; import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; /** * Runtime context for an API invocation flowing through the integration pipeline. @@ -57,7 +53,7 @@ public class ApiInvocationContext { this.variables = new HashMap<>(); this.attributes = new HashMap<>(); this.stepResults = new ArrayList<>(); - this.requestHeaders = new HashMap<>(); + this.requestHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); this.requestQueryParams = new HashMap<>(); } @@ -73,7 +69,7 @@ public class ApiInvocationContext { copy.httpMethod = this.httpMethod; copy.requestPath = this.requestPath; copy.requestBody = this.requestBody; - copy.requestQueryParams.putAll(this.requestQueryParams); + copy.requestQueryParams.putAll(this.requestQueryParams); copy.responseBody = this.responseBody; copy.responseStatus = this.responseStatus; copy.responseMessage = this.responseMessage; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/AuthPolicyEvaluator.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/AuthPolicyEvaluator.java deleted file mode 100644 index fdc58a3a..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/AuthPolicyEvaluator.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.zt.plat.module.databus.framework.integration.gateway.policy; - -import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; -import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; - -/** - * Performs authentication / authorization policy evaluation for a request. - */ -public interface AuthPolicyEvaluator { - - void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context); - -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultAuthPolicyEvaluator.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultAuthPolicyEvaluator.java deleted file mode 100644 index f4f252dc..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultAuthPolicyEvaluator.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.zt.plat.module.databus.framework.integration.gateway.policy; - -import com.zt.plat.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; -import com.zt.plat.framework.common.exception.ServiceException; -import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; -import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; -import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_AUTH_UNAUTHORIZED; - -/** - * Basic authentication evaluator delegating token validation to system module. - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class DefaultAuthPolicyEvaluator implements AuthPolicyEvaluator { - - private static final String TOKEN_HEADER = "ZT-Auth-Token"; - - private final OAuth2TokenCommonApi oauth2TokenCommonApi; - - @Override - public void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context) { - ApiPolicyAuthDO authPolicy = aggregate.getAuthPolicy(); - if (authPolicy == null) { - return; - } - validateHeaderToken(context); - } - - private void validateHeaderToken(ApiInvocationContext context) { - Object rawHeader = context.getRequestHeaders().get(TOKEN_HEADER); - String token = rawHeader == null ? null : String.valueOf(rawHeader).trim(); - if (!StringUtils.hasText(token)) { - throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED); - } - try { - oauth2TokenCommonApi.checkAccessToken(token).getCheckedData(); - context.getAttributes().putIfAbsent("accessToken", token); - String bearerToken = token.startsWith("Bearer ") ? token : "Bearer " + token; - context.getRequestHeaders().putIfAbsent("Authorization", bearerToken); - } catch (ServiceException ex) { - log.warn("Access token validation failed: {}", ex.getMessage()); - throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED); - } catch (RuntimeException ex) { - log.error("Access token validation error", ex); - throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED); - } - } -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultRateLimitPolicyEvaluator.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultRateLimitPolicyEvaluator.java index 030be0ad..7ddc95be 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultRateLimitPolicyEvaluator.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultRateLimitPolicyEvaluator.java @@ -22,7 +22,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; /** - * Simple Redis-backed rate limit evaluator supporting fixed window counters. + * Redis 支撑的基础限流评估器,支持固定窗口计数。 */ @Slf4j @Component @@ -54,7 +54,7 @@ public class DefaultRateLimitPolicyEvaluator implements RateLimitPolicyEvaluator throw ServiceExceptionUtil.exception(API_RATE_LIMIT_EXCEEDED); } } catch (JsonProcessingException | DataAccessException ex) { - log.error("Rate limit evaluation failed for api {}", aggregate.getDefinition().getApiCode(), ex); + log.error("API {} 的限流评估失败", aggregate.getDefinition().getApiCode(), ex); throw ServiceExceptionUtil.exception(API_RATE_LIMIT_EVALUATION_FAILED); } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/CachedBodyHttpServletRequest.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/CachedBodyHttpServletRequest.java new file mode 100644 index 00000000..f722c8f1 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/CachedBodyHttpServletRequest.java @@ -0,0 +1,179 @@ +package com.zt.plat.module.databus.framework.integration.gateway.security; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * 简单的报文缓存包装,便于后续处理链重复读取解密后的内容。 + */ +public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { + + private final byte[] cachedBody; + private String characterEncoding; + private final Map> additionalHeaders; + + public CachedBodyHttpServletRequest(HttpServletRequest request, byte[] cachedBody) { + super(request); + this.cachedBody = cachedBody != null ? cachedBody : new byte[0]; + this.characterEncoding = request.getCharacterEncoding(); + this.additionalHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + + @Override + public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { + if (encoding != null) { + super.setCharacterEncoding(encoding); + } + this.characterEncoding = encoding; + } + + @Override + public String getCharacterEncoding() { + return characterEncoding != null ? characterEncoding : StandardCharsets.UTF_8.name(); + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream delegate = new ByteArrayInputStream(cachedBody); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return delegate.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + // 保持空实现 + } + + @Override + public int read() { + return delegate.read(); + } + }; + } + + @Override + public BufferedReader getReader() throws IOException { + Charset charset = Charset.forName(getCharacterEncoding()); + return new BufferedReader(new InputStreamReader(getInputStream(), charset)); + } + + @Override + public int getContentLength() { + return cachedBody.length; + } + + @Override + public long getContentLengthLong() { + return cachedBody.length; + } + + public byte[] getCachedBody() { + return cachedBody.clone(); + } + + public void setHeader(String name, String value) { + if (!StringUtils.hasText(name)) { + return; + } + if (!StringUtils.hasText(value)) { + additionalHeaders.remove(name); + return; + } + additionalHeaders.put(name, new ArrayList<>(Collections.singletonList(value))); + } + + public void setHeaderValues(String name, List values) { + if (!StringUtils.hasText(name)) { + return; + } + if (CollectionUtils.isEmpty(values)) { + additionalHeaders.remove(name); + return; + } + additionalHeaders.put(name, new ArrayList<>(values)); + } + + public void addHeader(String name, String value) { + if (!StringUtils.hasText(name) || !StringUtils.hasText(value)) { + return; + } + additionalHeaders.compute(name, (key, existing) -> { + List list = existing == null ? new ArrayList<>() : new ArrayList<>(existing); + list.add(value); + return list; + }); + } + + public void removeHeader(String name) { + if (!StringUtils.hasText(name)) { + return; + } + additionalHeaders.remove(name); + } + + @Override + public String getHeader(String name) { + if (StringUtils.hasText(name)) { + List values = additionalHeaders.get(name); + if (!CollectionUtils.isEmpty(values)) { + return values.get(0); + } + } + return super.getHeader(name); + } + + @Override + public Enumeration getHeaders(String name) { + List combined = new ArrayList<>(); + if (StringUtils.hasText(name)) { + List custom = additionalHeaders.get(name); + if (!CollectionUtils.isEmpty(custom)) { + combined.addAll(custom); + } + } + Enumeration parent = super.getHeaders(name); + while (parent.hasMoreElements()) { + combined.add(parent.nextElement()); + } + return Collections.enumeration(combined); + } + + @Override + public Enumeration getHeaderNames() { + Set names = new LinkedHashSet<>(); + Enumeration parent = super.getHeaderNames(); + while (parent.hasMoreElements()) { + names.add(parent.nextElement()); + } + names.addAll(additionalHeaders.keySet()); + return Collections.enumeration(names); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewayJwtResolver.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewayJwtResolver.java new file mode 100644 index 00000000..6c8ff672 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewayJwtResolver.java @@ -0,0 +1,167 @@ +package com.zt.plat.module.databus.framework.integration.gateway.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.module.databus.framework.integration.gateway.core.GatewayHeaderUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; + +/** + * Shared helpers for resolving JWT tokens from incoming payloads so debug flows + * and external HTTP requests stay consistent. + */ +public final class GatewayJwtResolver { + + public static final String HEADER_ZT_AUTH_TOKEN = "ZT-Auth-Token"; + + private GatewayJwtResolver() { + } + + public static String resolveJwtToken(HttpServletRequest request, ObjectMapper objectMapper) { + if (request == null) { + return null; + } + String token = extractTokenFromHeader(request.getHeaders(HEADER_ZT_AUTH_TOKEN), objectMapper); + if (!StringUtils.hasText(token)) { + token = extractTokenFromHeader(request.getHeaders(HttpHeaders.AUTHORIZATION), objectMapper); + } + if (!StringUtils.hasText(token)) { + token = normalizeTokenValue(request.getParameter("token"), objectMapper); + } + return token; + } + + public static String resolveJwtToken(Map headers, + Map queryParams, + ObjectMapper objectMapper) { + String token = extractTokenFromMap(headers, HEADER_ZT_AUTH_TOKEN, objectMapper); + if (!StringUtils.hasText(token)) { + token = extractTokenFromMap(headers, HttpHeaders.AUTHORIZATION, objectMapper); + } + if (!StringUtils.hasText(token) && queryParams != null) { + token = normalizeTokenValue(queryParams.get("token"), objectMapper); + } + return token; + } + + private static String extractTokenFromHeader(Enumeration values, ObjectMapper objectMapper) { + if (values == null) { + return null; + } + while (values.hasMoreElements()) { + String candidate = normalizeTokenString(values.nextElement(), objectMapper); + if (StringUtils.hasText(candidate)) { + return candidate; + } + } + return null; + } + + private static String extractTokenFromMap(Map headers, String headerName, ObjectMapper objectMapper) { + if (headers == null || !StringUtils.hasText(headerName)) { + return null; + } + for (Map.Entry entry : headers.entrySet()) { + if (!headerName.equalsIgnoreCase(entry.getKey())) { + continue; + } + String candidate = normalizeTokenValue(entry.getValue(), objectMapper); + if (StringUtils.hasText(candidate)) { + return candidate; + } + } + return null; + } + + private static String normalizeTokenValue(Object value, ObjectMapper objectMapper) { + if (value == null) { + return null; + } + if (value instanceof String str) { + return normalizeTokenString(str, objectMapper); + } + if (value instanceof Map map) { + Object direct = map.get("token"); + if (direct == null) { + direct = map.get("accessToken"); + } + if (direct == null) { + direct = map.get("authToken"); + } + if (direct == null) { + direct = map.get("jwt"); + } + if (direct != null) { + String resolved = normalizeTokenValue(direct, objectMapper); + if (StringUtils.hasText(resolved)) { + return resolved; + } + } + for (Object entryValue : map.values()) { + String resolved = normalizeTokenValue(entryValue, objectMapper); + if (StringUtils.hasText(resolved)) { + return resolved; + } + } + return null; + } + if (value instanceof Iterable iterable) { + Iterator iterator = iterable.iterator(); + while (iterator.hasNext()) { + String resolved = normalizeTokenValue(iterator.next(), objectMapper); + if (StringUtils.hasText(resolved)) { + return resolved; + } + } + return null; + } + if (value.getClass().isArray()) { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + String resolved = normalizeTokenValue(Array.get(value, i), objectMapper); + if (StringUtils.hasText(resolved)) { + return resolved; + } + } + return null; + } + return normalizeTokenString(String.valueOf(value), objectMapper); + } + + private static String normalizeTokenString(String rawValue, ObjectMapper objectMapper) { + if (!StringUtils.hasText(rawValue)) { + return null; + } + String candidate = rawValue.trim(); + if (candidate.startsWith("\"") && candidate.endsWith("\"") && candidate.length() > 1) { + candidate = candidate.substring(1, candidate.length() - 1).trim(); + } + if ((candidate.startsWith("{") && candidate.endsWith("}")) + || (candidate.startsWith("[") && candidate.endsWith("]"))) { + candidate = parseStructuredToken(candidate, objectMapper); + } + if (!StringUtils.hasText(candidate)) { + return null; + } + return GatewayHeaderUtils.stripBearerPrefix(candidate); + } + + private static String parseStructuredToken(String candidate, ObjectMapper objectMapper) { + if (objectMapper == null) { + return candidate; + } + try { + Object parsed = objectMapper.readValue(candidate, Object.class); + String resolved = normalizeTokenValue(parsed, objectMapper); + return StringUtils.hasText(resolved) ? resolved : null; + } catch (IOException ex) { + return candidate; + } + } +} \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java index cbf6e548..34b47545 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java @@ -1,25 +1,39 @@ package com.zt.plat.module.databus.framework.integration.gateway.security; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; +import com.zt.plat.framework.common.util.servlet.ServletUtils; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO; import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; +import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.codec.digest.HmacUtils; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import org.springframework.util.AntPathMatcher; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; +import org.springframework.util.*; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** - * Security filter performing IP allow/deny, signature validation, and tenant extraction for the unified portal. + * 对进入网关的请求统一执行 IP 校验、报文签名、加解密与防重复校验。 */ @Slf4j @Component @@ -27,25 +41,73 @@ import java.util.List; public class GatewaySecurityFilter extends OncePerRequestFilter { private final ApiGatewayProperties properties; + private final StringRedisTemplate stringRedisTemplate; + private final ApiClientCredentialService credentialService; + private final ObjectMapper objectMapper; private final AntPathMatcher pathMatcher = new AntPathMatcher(); + private static final TypeReference> MAP_TYPE = new TypeReference<>() {}; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String requestPath = request.getRequestURI(); - if (!pathMatcher.match(properties.getBasePath() + "/**", requestPath)) { + String pathWithinApplication = pathWithinApplication(request); + boolean matchesPortalPath = properties.getAllBasePaths() + .stream() + .map(this::normalizeBasePath) + .anyMatch(basePath -> pathMatcher.match(basePath + "/**", pathWithinApplication)); + if (!matchesPortalPath) { filterChain.doFilter(request, response); return; } if (!isIpAllowed(request)) { - response.sendError(HttpStatus.FORBIDDEN.value(), "IP not allowed"); + log.warn("[API-PORTAL] 拦截来自 IP {} 访问 {} 的请求", request.getRemoteAddr(), pathWithinApplication); + response.sendError(HttpStatus.FORBIDDEN.value(), "IP 禁止访问"); return; } - if (properties.isEnableSignature() && !validateSignature(request)) { - response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid signature"); + ApiGatewayProperties.Security security = properties.getSecurity(); + ApiClientCredentialDO credential = null; + if (!security.isEnabled()) { + filterChain.doFilter(request, response); return; } - filterChain.doFilter(request, response); + try { + Long tenantId = resolveTenantId(request); + String appId = requireHeader(request, security.getAppIdHeader(), "缺少应用标识"); + credential = credentialService.findActiveCredential(appId) + .orElseThrow(() -> new SecurityValidationException(HttpStatus.UNAUTHORIZED, "应用凭证不存在或已禁用")); + + String timestampHeader = requireHeader(request, security.getTimestampHeader(), "缺少时间戳"); + validateTimestamp(timestampHeader, security); + String nonce = requireHeader(request, security.getNonceHeader(), "缺少随机数"); + if (nonce.length() < 8) { + throw new SecurityValidationException(HttpStatus.BAD_REQUEST, "随机数长度不足"); + } + String signature = requireHeader(request, security.getSignatureHeader(), "缺少签名"); + + byte[] originalBody = StreamUtils.copyToByteArray(request.getInputStream()); + byte[] decryptedBody = decryptRequestBody(originalBody, credential, security); + verifySignature(request, decryptedBody, signature, credential, security); + ensureNonce(tenantId, appId, nonce, security); + + CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody); + if (StringUtils.hasText(request.getCharacterEncoding())) { + securedRequest.setCharacterEncoding(request.getCharacterEncoding()); + } + propagateJwtToken(request, securedRequest); + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + try { + filterChain.doFilter(securedRequest, responseWrapper); + encryptResponse(responseWrapper, credential, security); + } finally { + responseWrapper.copyBodyToResponse(); + } + } catch (SecurityValidationException ex) { + log.warn("[API-PORTAL] 安全校验失败: {}", ex.getMessage()); + writeErrorResponse(response, security, credential, ex.status(), ex.getMessage()); + } catch (Exception ex) { + log.error("[API-PORTAL] 处理安全校验时出现异常", ex); + writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败"); + } } private boolean isIpAllowed(HttpServletRequest request) { @@ -58,18 +120,321 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { return CollectionUtils.isEmpty(allowed) || allowed.contains(remoteIp); } - private boolean validateSignature(HttpServletRequest request) { - String headerSignature = request.getHeader(properties.getSignatureHeader()); - if (!StringUtils.hasText(headerSignature)) { + private String pathWithinApplication(HttpServletRequest request) { + String requestUri = request.getRequestURI(); + String contextPath = request.getContextPath(); + if (StringUtils.hasText(contextPath) && requestUri.startsWith(contextPath)) { + return requestUri.substring(contextPath.length()); + } + return requestUri; + } + + private String normalizeBasePath(String basePath) { + String candidate = StringUtils.hasText(basePath) ? basePath : ApiGatewayProperties.DEFAULT_BASE_PATH; + candidate = candidate.startsWith("/") ? candidate : "/" + candidate; + if (candidate.endsWith("/")) { + candidate = candidate.substring(0, candidate.length() - 1); + } + return candidate; + } + + private Long resolveTenantId(HttpServletRequest request) { + if (!properties.isEnableTenantHeader()) { + return null; + } + String tenantHeader = request.getHeader(properties.getTenantHeader()); + if (!StringUtils.hasText(tenantHeader)) { + return null; + } + try { + return Long.valueOf(tenantHeader.trim()); + } catch (NumberFormatException ex) { + throw new SecurityValidationException(HttpStatus.BAD_REQUEST, "租户标识格式不正确"); + } + } + + private String requireHeader(HttpServletRequest request, String headerName, String message) { + String value = request.getHeader(headerName); + if (!StringUtils.hasText(value)) { + throw new SecurityValidationException(HttpStatus.BAD_REQUEST, message); + } + return value.trim(); + } + + private void validateTimestamp(String rawTimestamp, ApiGatewayProperties.Security security) { + long parsed; + try { + parsed = Long.parseLong(rawTimestamp.trim()); + } catch (NumberFormatException ex) { + throw new SecurityValidationException(HttpStatus.BAD_REQUEST, "时间戳格式错误"); + } + long now = System.currentTimeMillis(); + long skew = Math.abs(now - parsed); + if (skew > security.getAllowedClockSkewSeconds() * 1000) { + throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "请求到达时间超出 300s"); + } + } + + private byte[] decryptRequestBody(byte[] originalBody, + ApiClientCredentialDO credential, + ApiGatewayProperties.Security security) { + if (originalBody == null || originalBody.length == 0) { + return new byte[0]; + } + String payload = new String(originalBody, StandardCharsets.UTF_8).trim(); + if (!StringUtils.hasText(payload)) { + return new byte[0]; + } + String encryptionKey = credential.getEncryptionKey(); + String encryptionType = resolveEncryptionType(credential, security); + boolean canDecrypt = StringUtils.hasText(encryptionKey) && StringUtils.hasText(encryptionType); + if (!canDecrypt) { + if (security.isRequireBodyEncryption()) { + throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "应用未配置加密密钥"); + } + return originalBody; + } + try { + String decrypted = CryptoSignatureUtils.decrypt(payload, encryptionKey, encryptionType); + if (!StringUtils.hasText(decrypted)) { + return new byte[0]; + } + return decrypted.getBytes(StandardCharsets.UTF_8); + } catch (IllegalArgumentException | IllegalStateException ex) { + log.debug("[API-PORTAL] 解密请求报文失败", ex); + if (security.isRequireBodyEncryption()) { + throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "报文解密失败"); + } + return originalBody; + } + } + + private void verifySignature(HttpServletRequest request, + byte[] decryptedBody, + String signature, + ApiClientCredentialDO credential, + ApiGatewayProperties.Security security) { + Map signaturePayload = new LinkedHashMap<>(); + mergeQueryParameters(signaturePayload, request); + mergeBodyParameters(signaturePayload, decryptedBody); + signaturePayload.put("signature", signature); + + String signatureType = resolveSignatureType(credential, security); + try { + boolean valid = CryptoSignatureUtils.verifySignature(signaturePayload, signatureType); + if (!valid) { + throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "签名校验失败"); + } + } catch (IllegalArgumentException ex) { + throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "签名算法配置异常"); + } + } + + private void mergeQueryParameters(Map target, HttpServletRequest request) { + String queryString = request.getQueryString(); + if (!StringUtils.hasText(queryString)) { + return; + } + try { + MultiValueMap params = UriComponentsBuilder.newInstance() + .query(queryString) + .build() + .getQueryParams(); + params.forEach((key, values) -> { + if (!StringUtils.hasText(key) || "signature".equalsIgnoreCase(key)) { + return; + } + if (CollectionUtils.isEmpty(values)) { + target.put(key, ""); + } else if (values.size() == 1) { + target.put(key, values.get(0)); + } else { + target.put(key, String.join(",", values)); + } + }); + } catch (IllegalArgumentException ex) { + log.debug("[API-PORTAL] 解析查询串 {} 失败", queryString, ex); + target.put("query", queryString); + } + } + + private void mergeBodyParameters(Map target, byte[] body) { + if (body == null || body.length == 0) { + return; + } + String bodyText = new String(body, StandardCharsets.UTF_8).trim(); + if (!StringUtils.hasText(bodyText)) { + return; + } + if (bodyText.startsWith("{")) { + try { + Map bodyMap = objectMapper.readValue(bodyText, MAP_TYPE); + bodyMap.forEach((key, value) -> target.put(key, normalizeValue(value))); + return; + } catch (JsonProcessingException ex) { + log.debug("[API-PORTAL] 解析请求体 JSON 失败", ex); + } + } + target.put("body", bodyText); + } + + private Object normalizeValue(Object value) { + if (value == null) { + return null; + } + if (value instanceof Map || value instanceof List) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException ex) { + return value.toString(); + } + } + return value; + } + + private String resolveEncryptionType(ApiClientCredentialDO credential, ApiGatewayProperties.Security security) { + if (credential != null && StringUtils.hasText(credential.getEncryptionType())) { + return credential.getEncryptionType(); + } + return security.getEncryptionType(); + } + + private String resolveSignatureType(ApiClientCredentialDO credential, ApiGatewayProperties.Security security) { + if (credential != null && StringUtils.hasText(credential.getSignatureType())) { + return credential.getSignatureType(); + } + return security.getSignatureType(); + } + + private void ensureNonce(Long tenantId, String appId, String nonce, ApiGatewayProperties.Security security) { + long ttl = security.getNonceTtlSeconds(); + if (ttl <= 0) { + return; + } + String tenantSegment = tenantId != null ? tenantId.toString() : "global"; + String key = security.getNonceRedisKeyPrefix() + tenantSegment + ":" + appId + ":" + nonce; + try { + Boolean stored = stringRedisTemplate.opsForValue() + .setIfAbsent(key, "1", Duration.ofSeconds(ttl)); + if (Boolean.FALSE.equals(stored)) { + throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "重复请求"); + } + } catch (RuntimeException ex) { + log.error("[API-PORTAL] 校验随机数时出现异常", ex); + throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "重复请求校验失败"); + } + } + + private void encryptResponse(ContentCachingResponseWrapper responseWrapper, + ApiClientCredentialDO credential, + ApiGatewayProperties.Security security) throws IOException { + if (!security.isEncryptResponse()) { + return; + } + String encryptionKey = credential.getEncryptionKey(); + String encryptionType = resolveEncryptionType(credential, security); + if (!StringUtils.hasText(encryptionKey) || !StringUtils.hasText(encryptionType)) { + throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "应用未配置加密密钥"); + } + byte[] plainBody = responseWrapper.getContentAsByteArray(); + String charsetName = responseWrapper.getCharacterEncoding(); + if (!StringUtils.hasText(charsetName)) { + charsetName = StandardCharsets.UTF_8.name(); + } + Charset charset; + try { + charset = Charset.forName(charsetName); + } catch (IllegalArgumentException ex) { + charset = StandardCharsets.UTF_8; + } + String plainText = new String(plainBody, charset); + try { + String encrypted = CryptoSignatureUtils.encrypt(plainText, encryptionKey, encryptionType); + responseWrapper.resetBuffer(); + if (!StringUtils.hasText(responseWrapper.getContentType())) { + responseWrapper.setContentType("text/plain;charset=UTF-8"); + } + byte[] encryptedBytes = encrypted.getBytes(StandardCharsets.UTF_8); + responseWrapper.setContentLength(encryptedBytes.length); + responseWrapper.getOutputStream().write(encryptedBytes); + } catch (IllegalArgumentException | IllegalStateException ex) { + log.error("[API-PORTAL] 响应加密失败", ex); + throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "响应加密失败"); + } + } + + private void propagateJwtToken(HttpServletRequest originalRequest, CachedBodyHttpServletRequest securedRequest) { + String token = GatewayJwtResolver.resolveJwtToken(originalRequest, objectMapper); + if (!StringUtils.hasText(token)) { + return; + } + securedRequest.setHeader(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, token); + securedRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); + } + + private static final class SecurityValidationException extends RuntimeException { + + private final HttpStatus status; + + private SecurityValidationException(HttpStatus status, String message) { + super(message); + this.status = status; + } + + private HttpStatus status() { + return status; + } + } + + private void writeErrorResponse(HttpServletResponse response, + ApiGatewayProperties.Security security, + ApiClientCredentialDO credential, + HttpStatus status, + String message) { + if (response.isCommitted()) { + log.warn("[API-PORTAL] 响应已提交,无法写入安全校验错误: {}", message); + return; + } + response.resetBuffer(); + response.setStatus(status.value()); + String resolvedMessage = StringUtils.hasText(message) ? message : status.getReasonPhrase(); + ApiGatewayResponse envelope = ApiGatewayResponse.builder() + .code(status.value()) + .message(resolvedMessage) + .response(null) + .traceId(null) + .build(); + if (shouldEncryptErrorResponse(security, credential)) { + String encryptionKey = credential.getEncryptionKey(); + String encryptionType = resolveEncryptionType(credential, security); + try { + String json = objectMapper.writeValueAsString(envelope); + String encrypted = CryptoSignatureUtils.encrypt(json, encryptionKey, encryptionType); + response.setContentType("text/plain;charset=UTF-8"); + byte[] encryptedBytes = encrypted.getBytes(StandardCharsets.UTF_8); + response.setContentLength(encryptedBytes.length); + response.getOutputStream().write(encryptedBytes); + return; + } catch (JsonProcessingException ex) { + log.error("[API-PORTAL] 序列化安全错误响应失败", ex); + } catch (IllegalArgumentException | IllegalStateException ex) { + log.error("[API-PORTAL] 安全错误响应加密失败", ex); + } catch (IOException ex) { + log.error("[API-PORTAL] 写入加密安全响应失败", ex); + } + } + ServletUtils.writeJSON(response, envelope); + } + + private boolean shouldEncryptErrorResponse(ApiGatewayProperties.Security security, ApiClientCredentialDO credential) { + if (security == null || credential == null) { return false; } - String secret = properties.getSignatureSecret(); - if (!StringUtils.hasText(secret)) { - log.warn("Signature verification enabled but no secret configured"); + if (!security.isEncryptResponse()) { return false; } - String payload = request.getRequestURI() + "|" + (request.getQueryString() == null ? "" : request.getQueryString()); - String computed = HmacUtils.hmacSha256Hex(secret, payload); - return headerSignature.equalsIgnoreCase(computed); + String encryptionKey = credential.getEncryptionKey(); + String encryptionType = resolveEncryptionType(credential, security); + return StringUtils.hasText(encryptionKey) && StringUtils.hasText(encryptionType); } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/SignatureValidationException.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/SignatureValidationException.java new file mode 100644 index 00000000..44063581 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/SignatureValidationException.java @@ -0,0 +1,18 @@ +package com.zt.plat.module.databus.framework.integration.gateway.security; + +/** + * Exception thrown when signature verification fails. + */ +public class SignatureValidationException extends RuntimeException { + + private final int status; + + public SignatureValidationException(int status, String message) { + super(message); + this.status = status; + } + + public int getStatus() { + return status; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/StepHandlerFactory.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/StepHandlerFactory.java index 6038be14..e60f217b 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/StepHandlerFactory.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/StepHandlerFactory.java @@ -8,6 +8,7 @@ import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocat import lombok.RequiredArgsConstructor; import org.springframework.integration.core.GenericHandler; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import java.util.List; @@ -23,7 +24,8 @@ public class StepHandlerFactory { private final List stepHandlers; public GenericHandler build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) { - ApiStepTypeEnum type = ApiStepTypeEnum.valueOf(stepDefinition.getStep().getType().toUpperCase()); + String rawType = stepDefinition.getStep().getType(); + ApiStepTypeEnum type = resolveType(rawType); return stepHandlers.stream() .filter(handler -> handler.supports(type.name())) .findFirst() @@ -31,4 +33,15 @@ public class StepHandlerFactory { .build(aggregate, stepDefinition); } + private ApiStepTypeEnum resolveType(String rawType) { + if (!StringUtils.hasText(rawType)) { + throw ServiceExceptionUtil.exception(API_STEP_UNSUPPORTED_TYPE, ""); + } + for (ApiStepTypeEnum candidate : ApiStepTypeEnum.values()) { + if (candidate.name().equalsIgnoreCase(rawType)) { + return candidate; + } + } + throw ServiceExceptionUtil.exception(API_STEP_UNSUPPORTED_TYPE, rawType); + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/EndStepHandler.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/EndStepHandler.java new file mode 100644 index 00000000..d1c90a38 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/EndStepHandler.java @@ -0,0 +1,74 @@ +package com.zt.plat.module.databus.framework.integration.gateway.step.impl; + +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; +import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpec; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpecParser; +import com.zt.plat.module.databus.framework.integration.gateway.expression.GatewayExpressionHelper; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiStepResult; +import com.zt.plat.module.databus.framework.integration.gateway.step.ApiStepHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.integration.core.GenericHandler; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_END_EXECUTION_FAILED; + +/** + * Handler for END orchestration nodes. + */ +@Component +@RequiredArgsConstructor +public class EndStepHandler implements ApiStepHandler { + + private final ExpressionExecutor expressionExecutor; + + @Override + public boolean supports(String stepType) { + return "END".equalsIgnoreCase(stepType); + } + + @Override + public GenericHandler build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) { + return (payload, headers) -> { + Instant start = Instant.now(); + Object snapshotBefore = GatewayExpressionHelper.snapshotResponse(payload); + try { + ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getResponseMappingExpr(), ExpressionTypeEnum.JSON); + if (spec != null) { + Object evaluated = expressionExecutor.evaluate(spec, payload, payload.getResponseBody(), headers); + GatewayExpressionHelper.applyContextMutations(payload, evaluated, false, true); + } + payload.addStepResult(ApiStepResult.builder() + .stepId(stepDefinition.getStep().getId()) + .stepType(stepDefinition.getStep().getType()) + .request(snapshotBefore) + .response(GatewayExpressionHelper.snapshotResponse(payload)) + .success(true) + .elapsed(Duration.between(start, Instant.now())) + .build()); + } catch (Exception ex) { + payload.addStepResult(ApiStepResult.builder() + .stepId(stepDefinition.getStep().getId()) + .stepType(stepDefinition.getStep().getType()) + .request(snapshotBefore) + .success(false) + .errorMessage(ex.getMessage()) + .elapsed(Duration.between(start, Instant.now())) + .build()); + if (ex instanceof ServiceException serviceException) { + throw serviceException; + } + throw ServiceExceptionUtil.exception(API_STEP_END_EXECUTION_FAILED, ex.getMessage()); + } + return payload; + }; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java index 10323696..bf75d09a 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java @@ -27,13 +27,7 @@ import reactor.core.publisher.Mono; import java.net.URI; import java.time.Duration; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_HTTP_ENDPOINT_INVALID; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_HTTP_EXECUTION_FAILED; @@ -383,13 +377,7 @@ public class HttpStepHandler implements ApiStepHandler { } private boolean supportsRequestBody(HttpMethod method) { - if (method == null) { - return true; - } - return !(HttpMethod.GET.equals(method) - || HttpMethod.DELETE.equals(method) - || HttpMethod.HEAD.equals(method) - || HttpMethod.OPTIONS.equals(method) - || HttpMethod.TRACE.equals(method)); + // 所有请求都要传递请求体 + return true; } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java new file mode 100644 index 00000000..db6d83e9 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java @@ -0,0 +1,73 @@ +package com.zt.plat.module.databus.framework.integration.gateway.step.impl; + +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; +import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpec; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpecParser; +import com.zt.plat.module.databus.framework.integration.gateway.expression.GatewayExpressionHelper; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiStepResult; +import com.zt.plat.module.databus.framework.integration.gateway.step.ApiStepHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.integration.core.GenericHandler; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_START_EXECUTION_FAILED; + +/** + * Handler for START orchestration nodes. + */ +@Component +@RequiredArgsConstructor +public class StartStepHandler implements ApiStepHandler { + + private final ExpressionExecutor expressionExecutor; + + @Override + public boolean supports(String stepType) { + return "START".equalsIgnoreCase(stepType); + } + + @Override + public GenericHandler build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) { + return (payload, headers) -> { + Instant start = Instant.now(); + Object snapshotBefore = GatewayExpressionHelper.snapshotRequest(payload); + try { + ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getRequestMappingExpr(), ExpressionTypeEnum.JSON); + if (spec != null) { + Object evaluated = expressionExecutor.evaluate(spec, payload, payload.getRequestBody(), headers); + GatewayExpressionHelper.applyContextMutations(payload, evaluated, true, false); + } + payload.addStepResult(ApiStepResult.builder() + .stepId(stepDefinition.getStep().getId()) + .stepType(stepDefinition.getStep().getType()) + .request(snapshotBefore) + .response(GatewayExpressionHelper.snapshotRequest(payload)) + .success(true) + .elapsed(Duration.between(start, Instant.now())) + .build()); + } catch (Exception ex) { + payload.addStepResult(ApiStepResult.builder() + .stepId(stepDefinition.getStep().getId()) + .stepType(stepDefinition.getStep().getType()) + .request(snapshotBefore) + .success(false) + .errorMessage(ex.getMessage()) + .elapsed(Duration.between(start, Instant.now())) + .build()); + if (ex instanceof ServiceException serviceException) { + throw serviceException; + } + throw ServiceExceptionUtil.exception(API_STEP_START_EXECUTION_FAILED, ex.getMessage()); + } + return payload; + }; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiClientCredentialService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiClientCredentialService.java new file mode 100644 index 00000000..11234ee6 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiClientCredentialService.java @@ -0,0 +1,26 @@ +package com.zt.plat.module.databus.service.gateway; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialPageReqVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialSaveReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO; + +import java.util.List; +import java.util.Optional; + +public interface ApiClientCredentialService { + + PageResult getPage(ApiClientCredentialPageReqVO reqVO); + + Long create(ApiClientCredentialSaveReqVO reqVO); + + void update(ApiClientCredentialSaveReqVO reqVO); + + void delete(Long id); + + ApiClientCredentialDO get(Long id); + + List listEnabled(); + + Optional findActiveCredential(String appId); +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiPolicyAuthService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiPolicyAuthService.java deleted file mode 100644 index 7768186b..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiPolicyAuthService.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.zt.plat.module.databus.service.gateway; - -import com.zt.plat.framework.common.pojo.PageResult; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; - -import java.util.List; -import java.util.Optional; - -/** - * Authentication policy operations. - */ -public interface ApiPolicyAuthService { - - /** - * Paginate policies. - */ - PageResult getPage(ApiPolicyPageReqVO reqVO); - - /** - * Fetch all active policies for dropdowns. - */ - List getSimpleList(); - - /** - * Find policy detail. - */ - Optional get(Long id); - - /** - * Create policy definition. - */ - Long create(ApiPolicySaveReqVO reqVO); - - /** - * Update policy definition. - */ - void update(ApiPolicySaveReqVO reqVO); - - /** - * Delete policy definition. - */ - void delete(Long id); - -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiClientCredentialServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiClientCredentialServiceImpl.java new file mode 100644 index 00000000..76e32a30 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiClientCredentialServiceImpl.java @@ -0,0 +1,133 @@ +package com.zt.plat.module.databus.service.gateway.impl; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialPageReqVO; +import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialSaveReqVO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO; +import com.zt.plat.module.databus.dal.mysql.gateway.ApiClientCredentialMapper; +import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_DUPLICATE_APP; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_NOT_FOUND; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ApiClientCredentialServiceImpl implements ApiClientCredentialService { + + private final ApiClientCredentialMapper credentialMapper; + + private LoadingCache> credentialCache; + + @PostConstruct + public void initCache() { + credentialCache = Caffeine.newBuilder() + .maximumSize(256) + .expireAfterWrite(Duration.ofMinutes(5)) + .build(this::loadCredentialSync); + } + + @Override + public PageResult getPage(ApiClientCredentialPageReqVO reqVO) { + return credentialMapper.selectPage(reqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(ApiClientCredentialSaveReqVO reqVO) { + ensureAppIdUnique(reqVO.getAppId(), null); + + ApiClientCredentialDO credential = BeanUtils.toBean(reqVO, ApiClientCredentialDO.class); + credential.setId(null); + credential.setDeleted(Boolean.FALSE); + credentialMapper.insert(credential); + invalidateCache(credential.getAppId()); + return credential.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(ApiClientCredentialSaveReqVO reqVO) { + ApiClientCredentialDO existing = ensureExists(reqVO.getId()); + ensureAppIdUnique(reqVO.getAppId(), existing.getId()); + + ApiClientCredentialDO updateObj = BeanUtils.toBean(reqVO, ApiClientCredentialDO.class); + credentialMapper.updateById(updateObj); + invalidateCache(existing.getAppId()); + invalidateCache(updateObj.getAppId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + ApiClientCredentialDO existing = ensureExists(id); + credentialMapper.deleteById(id); + invalidateCache(existing.getAppId()); + } + + @Override + public ApiClientCredentialDO get(Long id) { + return ensureExists(id); + } + + @Override + public List listEnabled() { + return credentialMapper.selectEnabledList(); + } + + @Override + public Optional findActiveCredential(String appId) { + if (!StringUtils.hasText(appId)) { + return Optional.empty(); + } + return credentialCache.get(appId.trim()); + } + + private Optional loadCredentialSync(String appId) { + Optional credential = credentialMapper.selectByAppId(appId) + .filter(item -> Boolean.TRUE.equals(item.getEnabled())); + if (credential.isEmpty()) { + log.debug("[API-PORTAL] 未找到 appId={} 的有效凭证", appId); + } + return credential; + } + + private void ensureAppIdUnique(String appId, Long currentId) { + credentialMapper.selectByAppId(appId) + .filter(existing -> currentId == null || !Objects.equals(existing.getId(), currentId)) + .ifPresent(existing -> { throw ServiceExceptionUtil.exception(API_CREDENTIAL_DUPLICATE_APP); }); + } + + private ApiClientCredentialDO ensureExists(Long id) { + if (id == null) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_NOT_FOUND); + } + ApiClientCredentialDO credential = credentialMapper.selectById(id); + if (credential == null || Boolean.TRUE.equals(credential.getDeleted())) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_NOT_FOUND); + } + return credential; + } + + private void invalidateCache(String appId) { + if (!StringUtils.hasText(appId)) { + return; + } + credentialCache.invalidate(appId.trim()); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java index 01021e42..16eaa61b 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java @@ -16,13 +16,11 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefi import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionTransformSaveReqVO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiFlowPublishDO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO; import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiFlowPublishMapper; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyAuthMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyRateLimitMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper; @@ -58,7 +56,6 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { private final ApiDefinitionMapper apiDefinitionMapper; private final ApiStepMapper apiStepMapper; private final ApiTransformMapper apiTransformMapper; - private final ApiPolicyAuthMapper apiPolicyAuthMapper; private final ApiPolicyRateLimitMapper apiPolicyRateLimitMapper; private final ApiFlowPublishMapper apiFlowPublishMapper; private final ObjectMapper objectMapper; @@ -186,7 +183,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { ApiDefinitionAggregate aggregate = objectMapper.readValue(json, ApiDefinitionAggregate.class); return Optional.of(aggregate); } catch (JsonProcessingException | DataAccessException ex) { - log.warn("Failed to deserialize API definition aggregate from redis for key {}", cacheKey, ex); + log.warn("反序列化 Redis 中 key {} 的 API 定义聚合失败", cacheKey, ex); return Optional.empty(); } } @@ -196,7 +193,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { String json = objectMapper.writeValueAsString(aggregate); stringRedisTemplate.opsForValue().set(REDIS_CACHE_PREFIX + cacheKey, json, 5, TimeUnit.MINUTES); } catch (JsonProcessingException | DataAccessException ex) { - log.warn("Failed to persist API definition aggregate to redis for key {}", cacheKey, ex); + log.warn("将 API 定义聚合写入 Redis key {} 失败", cacheKey, ex); } } @@ -204,7 +201,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { try { stringRedisTemplate.delete(REDIS_CACHE_PREFIX + cacheKey); } catch (DataAccessException ex) { - log.warn("Failed to delete API definition aggregate from redis for key {}", cacheKey, ex); + log.warn("删除 Redis 中 key {} 的 API 定义聚合失败", cacheKey, ex); } } @@ -219,7 +216,6 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { for (ApiStepDO stepDO : stepDOS) { List transforms = convertTransforms(apiTransformMapper.selectByStepId(stepDO.getId())); Map metadata = new LinkedHashMap<>(); - metadata.put("retryStrategy", parseJson(stepDO.getRetryStrategy())); metadata.put("fallbackStrategy", parseJson(stepDO.getFallbackStrategy())); metadata.put("timeout", stepDO.getTimeout()); metadata.put("stopOnError", stepDO.getStopOnError()); @@ -234,9 +230,6 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { for (ApiTransformDefinition transform : convertTransforms(apiTransformMapper.selectApiLevelTransforms(definition.getId()))) { apiTransforms.put(transform.getPhase(), transform); } - ApiPolicyAuthDO authPolicy = Optional.ofNullable(definition.getAuthPolicyId()) - .map(apiPolicyAuthMapper::selectById) - .orElse(null); ApiPolicyRateLimitDO rateLimitPolicy = Optional.ofNullable(definition.getRateLimitId()) .map(apiPolicyRateLimitMapper::selectById) .orElse(null); @@ -247,7 +240,6 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { .definition(definition) .steps(stepDefinitions) .apiLevelTransforms(apiTransforms) - .authPolicy(authPolicy) .rateLimitPolicy(rateLimitPolicy) .publication(publication) .build(); @@ -277,7 +269,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { try { return objectMapper.readValue(json, new TypeReference>() {}); } catch (JsonProcessingException ex) { - log.warn("Failed to parse configuration JSON: {}", json, ex); + log.warn("解析配置 JSON 失败: {}", json, ex); return Collections.emptyMap(); } } @@ -331,6 +323,21 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { ApiStepDO stepDO = BeanUtils.toBean(stepVO, ApiStepDO.class); stepDO.setId(null); stepDO.setApiId(apiId); + if (isStartStep(stepVO)) { + stepDO.setParallelGroup(null); + stepDO.setTargetEndpoint(null); + stepDO.setFallbackStrategy(null); + stepDO.setConditionExpr(null); + stepDO.setStopOnError(Boolean.FALSE); + stepDO.setTimeout(null); + } else if (isEndStep(stepVO)) { + stepDO.setParallelGroup(null); + stepDO.setTargetEndpoint(null); + stepDO.setFallbackStrategy(null); + stepDO.setConditionExpr(null); + stepDO.setStopOnError(Boolean.FALSE); + stepDO.setTimeout(null); + } applyTenantDefaults(stepDO); apiStepMapper.insert(stepDO); persistStepTransforms(apiId, stepDO.getId(), stepVO.getTransforms()); @@ -377,14 +384,43 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { if (CollUtil.isEmpty(reqVO.getSteps())) { throw ServiceExceptionUtil.exception(API_DEFINITION_STEP_EMPTY); } + if (reqVO.getSteps().size() < 2) { + throw ServiceExceptionUtil.exception(API_DEFINITION_STEP_EMPTY); + } Set orders = new HashSet<>(); + ApiDefinitionStepSaveReqVO startStep = null; + ApiDefinitionStepSaveReqVO endStep = null; for (ApiDefinitionStepSaveReqVO step : reqVO.getSteps()) { Integer order = step.getStepOrder(); - if (order == null || !orders.add(order)) { + if (order == null || order <= 0 || !orders.add(order)) { throw ServiceExceptionUtil.exception(API_DEFINITION_STEP_ORDER_DUPLICATE); } + if (isStartStep(step)) { + if (startStep != null) { + throw ServiceExceptionUtil.exception(API_DEFINITION_START_STEP_DUPLICATE); + } + startStep = step; + } else if (isEndStep(step)) { + if (endStep != null) { + throw ServiceExceptionUtil.exception(API_DEFINITION_END_STEP_DUPLICATE); + } + endStep = step; + } validateTransformPhases(step.getTransforms()); } + if (startStep == null) { + throw ServiceExceptionUtil.exception(API_DEFINITION_START_STEP_REQUIRED); + } + if (endStep == null) { + throw ServiceExceptionUtil.exception(API_DEFINITION_END_STEP_REQUIRED); + } + if (!Objects.equals(startStep.getStepOrder(), 1)) { + throw ServiceExceptionUtil.exception(API_DEFINITION_START_STEP_INVALID); + } + int expectedEndOrder = reqVO.getSteps().size(); + if (!Objects.equals(endStep.getStepOrder(), expectedEndOrder)) { + throw ServiceExceptionUtil.exception(API_DEFINITION_END_STEP_INVALID); + } validateTransformPhases(reqVO.getApiLevelTransforms()); } @@ -401,10 +437,15 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { } } + private boolean isStartStep(ApiDefinitionStepSaveReqVO step) { + return StringUtils.hasText(step.getType()) && "START".equalsIgnoreCase(step.getType()); + } + + private boolean isEndStep(ApiDefinitionStepSaveReqVO step) { + return StringUtils.hasText(step.getType()) && "END".equalsIgnoreCase(step.getType()); + } + private void validatePolicies(ApiDefinitionSaveReqVO reqVO) { - if (reqVO.getAuthPolicyId() != null && apiPolicyAuthMapper.selectById(reqVO.getAuthPolicyId()) == null) { - throw ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND); - } if (reqVO.getRateLimitId() != null && apiPolicyRateLimitMapper.selectById(reqVO.getRateLimitId()) == null) { throw ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND); } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyAuthServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyAuthServiceImpl.java deleted file mode 100644 index 00a9e953..00000000 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyAuthServiceImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.zt.plat.module.databus.service.gateway.impl; - -import cn.hutool.core.util.StrUtil; -import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; -import com.zt.plat.framework.common.pojo.PageResult; -import com.zt.plat.framework.tenant.core.context.TenantContextHolder; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO; -import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyAuthMapper; -import com.zt.plat.module.databus.service.gateway.ApiPolicyAuthService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.Assert; - -import java.util.List; -import java.util.Optional; - -import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_IN_USE; -import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_NOT_FOUND; - -@Service -@RequiredArgsConstructor -public class ApiPolicyAuthServiceImpl implements ApiPolicyAuthService { - - private final ApiPolicyAuthMapper authMapper; - private final ApiDefinitionMapper apiDefinitionMapper; - - @Override - public PageResult getPage(ApiPolicyPageReqVO reqVO) { - return authMapper.selectPage(reqVO); - } - - @Override - public List getSimpleList() { - return authMapper.selectSimpleList(); - } - - @Override - public Optional get(Long id) { - if (id == null) { - return Optional.empty(); - } - return Optional.ofNullable(authMapper.selectById(id)) - .filter(policy -> !Boolean.TRUE.equals(policy.getDeleted())); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public Long create(ApiPolicySaveReqVO reqVO) { - ApiPolicyAuthDO policy = new ApiPolicyAuthDO(); - apply(reqVO, policy); - policy.setId(null); - policy.setTenantId(TenantContextHolder.getTenantId()); - policy.setDeleted(Boolean.FALSE); - authMapper.insert(policy); - return policy.getId(); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void update(ApiPolicySaveReqVO reqVO) { - ApiPolicyAuthDO existing = ensureExists(reqVO.getId()); - apply(reqVO, existing); - authMapper.updateById(existing); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void delete(Long id) { - ApiPolicyAuthDO existing = ensureExists(id); - Long referenceCount = apiDefinitionMapper.selectCountByAuthPolicyId(existing.getId()); - if (referenceCount != null && referenceCount > 0) { - throw ServiceExceptionUtil.exception(API_POLICY_IN_USE); - } - authMapper.deleteById(existing.getId()); - } - - private ApiPolicyAuthDO ensureExists(Long id) { - Assert.notNull(id, "策略编号不能为空"); - return get(id).orElseThrow(() -> ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND)); - } - - private void apply(ApiPolicySaveReqVO reqVO, ApiPolicyAuthDO target) { - target.setName(StrUtil.trim(reqVO.getName())); - target.setType(StrUtil.trim(reqVO.getType())); - target.setConfig(normalizeNullable(reqVO.getConfig())); - target.setDescription(normalizeNullable(reqVO.getDescription())); - } - - private String normalizeNullable(String value) { - String trimmed = StrUtil.trim(value); - return StrUtil.isEmpty(trimmed) ? null : trimmed; - } - -} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java index a2df8f3d..d5e1e815 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java @@ -9,11 +9,17 @@ public interface GatewayServiceErrorCodeConstants { ErrorCode API_DEFINITION_NOT_FOUND = new ErrorCode(1_010_000_001, "API 定义未发布或已下线"); ErrorCode API_DEFINITION_DUPLICATE = new ErrorCode(1_010_000_002, "API 编码与版本已存在"); - ErrorCode API_DEFINITION_STEP_EMPTY = new ErrorCode(1_010_000_003, "至少需要配置一个编排步骤"); + ErrorCode API_DEFINITION_STEP_EMPTY = new ErrorCode(1_010_000_003, "至少需要包含开始和结束节点"); ErrorCode API_DEFINITION_STEP_ORDER_DUPLICATE = new ErrorCode(1_010_000_004, "步骤序号重复"); ErrorCode API_TRANSFORM_PHASE_DUPLICATE = new ErrorCode(1_010_000_005, "同一级别的变换阶段重复"); ErrorCode API_POLICY_NOT_FOUND = new ErrorCode(1_010_000_006, "绑定的策略不存在"); - ErrorCode API_POLICY_IN_USE = new ErrorCode(1_010_000_028, "策略已被 API 定义引用,无法删除"); + ErrorCode API_POLICY_IN_USE = new ErrorCode(1_010_000_028, "策略已被 API 定义引用,无法删除"); + ErrorCode API_DEFINITION_START_STEP_REQUIRED = new ErrorCode(1_010_000_029, "必须包含开始节点"); + ErrorCode API_DEFINITION_END_STEP_REQUIRED = new ErrorCode(1_010_000_030, "必须包含结束节点"); + ErrorCode API_DEFINITION_START_STEP_INVALID = new ErrorCode(1_010_000_031, "开始节点必须位于第一个位置"); + ErrorCode API_DEFINITION_END_STEP_INVALID = new ErrorCode(1_010_000_032, "结束节点必须位于最后一个位置"); + ErrorCode API_DEFINITION_START_STEP_DUPLICATE = new ErrorCode(1_010_000_035, "开始节点只能存在一个"); + ErrorCode API_DEFINITION_END_STEP_DUPLICATE = new ErrorCode(1_010_000_036, "结束节点只能存在一个"); ErrorCode API_FLOW_NOT_FOUND = new ErrorCode(1_010_000_007, "未找到可用的 API 调度流程:code={}, version={}"); ErrorCode API_FLOW_NO_REPLY = new ErrorCode(1_010_000_008, "集成流程未返回响应:code={}, version={}"); ErrorCode API_AUTH_UNAUTHORIZED = new ErrorCode(1_010_000_009, "请求未通过认证"); @@ -35,5 +41,16 @@ public interface GatewayServiceErrorCodeConstants { ErrorCode API_JSONATA_BIND_FAILED = new ErrorCode(1_010_000_025, "表达式环境绑定失败"); ErrorCode API_STEP_EXECUTION_ERROR = new ErrorCode(1_010_000_026, "步骤执行出现异常"); ErrorCode API_STEP_UNSUPPORTED_TYPE = new ErrorCode(1_010_000_027, "不支持的步骤类型:{}"); + ErrorCode API_STEP_START_EXECUTION_FAILED = new ErrorCode(1_010_000_033, "开始节点执行失败:{}"); + ErrorCode API_STEP_END_EXECUTION_FAILED = new ErrorCode(1_010_000_034, "结束节点执行失败:{}"); + ErrorCode API_SIGNATURE_MISSING_HEADERS = new ErrorCode(1_010_000_037, "签名验证缺少必要头信息"); + ErrorCode API_SIGNATURE_INVALID_TIMESTAMP = new ErrorCode(1_010_000_038, "签名时间戳不合法"); + ErrorCode API_SIGNATURE_EXPIRED = new ErrorCode(1_010_000_039, "签名时间戳已过期"); + ErrorCode API_SIGNATURE_APP_NOT_FOUND = new ErrorCode(1_010_000_040, "签名应用不存在或已禁用"); + ErrorCode API_SIGNATURE_INVALID = new ErrorCode(1_010_000_041, "签名校验失败"); + ErrorCode API_SIGNATURE_NONCE_REPLAY = new ErrorCode(1_010_000_042, "签名随机串重复使用"); + ErrorCode API_SIGNATURE_CONFIG_INVALID = new ErrorCode(1_010_000_043, "签名策略配置异常"); + ErrorCode API_CREDENTIAL_NOT_FOUND = new ErrorCode(1_010_000_044, "应用凭证不存在或已删除"); + ErrorCode API_CREDENTIAL_DUPLICATE_APP = new ErrorCode(1_010_000_045, "应用标识已存在"); } diff --git a/zt-module-databus/zt-module-databus-server/src/main/resources/application.yml b/zt-module-databus/zt-module-databus-server/src/main/resources/application.yml index 01045a0c..d355c7a9 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/resources/application.yml +++ b/zt-module-databus/zt-module-databus-server/src/main/resources/application.yml @@ -108,16 +108,27 @@ zt: web: admin-ui: url: http://dashboard.zt.iocoder.cn # Admin 管理后台 UI 的地址 + security: + permit-all-urls: + - ${databus.api-portal.base-path:/admin-api/databus/api/portal}/** + - /admin-api/databus/api/portal/** + - /databus/api/portal/** xss: enable: false exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: 统一对外 API 网关 + description: 提供统一对外 API 网关 version: ${zt.info.version} tenant: # 多租户相关配置项 enable: true + ignore-urls: + - ${databus.api-portal.base-path:/admin-api/databus/api/portal}/** + - /admin-api/databus/api/portal/** + - /databus/api/portal/** + ignore-tables: + - databus_api_client_credential debug: false diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java new file mode 100644 index 00000000..b4c6207e --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java @@ -0,0 +1,63 @@ +package com.zt.plat.module.databus.controller.admin.gateway; + +import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO; +import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayExecutionService; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; +import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ApiGatewayController.class) +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(properties = { + "spring.config.import=optional:", + "spring.cloud.nacos.config.enabled=false", + "spring.cloud.nacos.discovery.enabled=false" +}) +class ApiGatewayControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ApiGatewayExecutionService executionService; + + @MockBean + private ApiDefinitionService apiDefinitionService; + + @Test + void invokeShouldReturnGatewayEnvelope() throws Exception { + ApiGatewayResponse response = ApiGatewayResponse.builder() + .code(200) + .message("OK") + .response(Map.of("code", 0)) + .traceId("trace-123") + .build(); + when(executionService.invokeForDebug(any(ApiGatewayInvokeReqVO.class))) + .thenReturn(ResponseEntity.ok(response)); + + mockMvc.perform(post("/databus/gateway/invoke") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"apiCode\":\"demo\",\"version\":\"v1\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("OK")) + .andExpect(jsonPath("$.response.code").value(0)) + .andExpect(jsonPath("$.traceId").value("trace-123")); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapperTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapperTest.java new file mode 100644 index 00000000..5307192b --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapperTest.java @@ -0,0 +1,105 @@ +package com.zt.plat.module.databus.framework.integration.gateway.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.HandlerMapping; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class ApiGatewayRequestMapperTest { + + private ApiGatewayRequestMapper mapper; + + @BeforeEach + void setUp() { + ApiGatewayProperties properties = new ApiGatewayProperties(); + mapper = new ApiGatewayRequestMapper(new ObjectMapper(), properties); + } + + @Test + void shouldUseUriTemplateVariablesWhenPresent() { + Map headers = new HashMap<>(); + headers.put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Map.of("apiCode", "demo.api", "version", "v1")); + headers.put(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, "GET"); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri", "/admin-api/databus/api/portal/demo.api/v1"); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders", Map.of(org.springframework.http.HttpHeaders.CONTENT_TYPE, org.springframework.http.MediaType.APPLICATION_JSON_VALUE)); + + ApiInvocationContext context = mapper.map("", headers); + + assertThat(context.getApiCode()).isEqualTo("demo.api"); + assertThat(context.getApiVersion()).isEqualTo("v1"); + assertThat(context.getRequestPath()).isEqualTo("/admin-api/databus/api/portal/demo.api/v1"); + assertThat(context.getHttpMethod()).isEqualTo("GET"); + } + + @Test + void shouldInferFromAbsoluteRequestUrlWhenVariablesMissing() { + Map headers = new HashMap<>(); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri", "http://localhost:48080/admin-api/databus/api/portal/system.auth.quick-login/v1?foo=bar"); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders", Map.of(org.springframework.http.HttpHeaders.CONTENT_TYPE, org.springframework.http.MediaType.TEXT_PLAIN_VALUE)); + + ApiInvocationContext context = mapper.map("payload", headers); + + assertThat(context.getApiCode()).isEqualTo("system.auth.quick-login"); + assertThat(context.getApiVersion()).isEqualTo("v1"); + assertThat(context.getRequestPath()).isEqualTo("/admin-api/databus/api/portal/system.auth.quick-login/v1"); + } + + @Test + void shouldFallbackToHeadersWhenAvailable() { + Map headers = new HashMap<>(); + headers.put("apiCode", "override.api"); + headers.put("version", "v3"); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri", "/another/path"); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders", Map.of(org.springframework.http.HttpHeaders.CONTENT_TYPE, org.springframework.http.MediaType.APPLICATION_JSON_VALUE)); + + ApiInvocationContext context = mapper.map("", headers); + + assertThat(context.getApiCode()).isEqualTo("override.api"); + assertThat(context.getApiVersion()).isEqualTo("v3"); + } + + @Test + void shouldNormalizeHeaderValuesAndSupportCaseInsensitiveLookup() { + Map headers = new HashMap<>(); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders", Map.of("ZT-Auth-Token", List.of("token-123"))); + + ApiInvocationContext context = mapper.map("", headers); + + assertThat(context.getRequestHeaders().get("ZT-Auth-Token")).isEqualTo("token-123"); + assertThat(context.getRequestHeaders().get("zt-auth-token")).isEqualTo("token-123"); + } + + @Test + void shouldExtractQueryParamsFromHeaders() { + Map headers = new HashMap<>(); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri", "/api/demo"); + headers.put("http_requestParams", Map.of( + "single", new String[]{"value"}, + "multi", List.of("a", "b") + )); + + ApiInvocationContext context = mapper.map("", headers); + + assertThat(context.getRequestQueryParams()).containsEntry("single", "value"); + assertThat(context.getRequestQueryParams().get("multi")).isEqualTo(List.of("a", "b")); + } + + @Test + void shouldFallbackToQueryStringWhenHeaderMissing() { + Map headers = new HashMap<>(); + headers.put(org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri", "/api/demo?foo=bar&arr=1&arr=2"); + + ApiInvocationContext context = mapper.map("", headers); + + assertThat(context.getRequestQueryParams()).containsEntry("foo", "bar"); + assertThat(context.getRequestQueryParams().get("arr")).isEqualTo(List.of("1", "2")); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultAuthPolicyEvaluatorTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultAuthPolicyEvaluatorTest.java new file mode 100644 index 00000000..3e583ece --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/policy/DefaultAuthPolicyEvaluatorTest.java @@ -0,0 +1,56 @@ +package com.zt.plat.module.databus.framework.integration.gateway.policy; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.module.databus.framework.integration.gateway.security.GatewayJwtResolver; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultAuthPolicyEvaluatorTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void shouldResolveTokenFromPrimaryHeader() { + Map headers = new HashMap<>(); + headers.put(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, List.of(" token-123 ")); + + String token = GatewayJwtResolver.resolveJwtToken(headers, Map.of(), objectMapper); + + assertThat(token).isEqualTo("token-123"); + } + + @Test + void shouldFallbackToAuthorizationHeader() { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer token-456"); + + String token = GatewayJwtResolver.resolveJwtToken(headers, Map.of(), objectMapper); + + assertThat(token).isEqualTo("token-456"); + } + + @Test + void shouldParseTokenFromStructuredPayload() { + Map headers = new HashMap<>(); + headers.put("Authorization", List.of("", "{\"token\":\"abc-789\"}")); + + String token = GatewayJwtResolver.resolveJwtToken(headers, Map.of(), objectMapper); + + assertThat(token).isEqualTo("abc-789"); + } + + @Test + void shouldUseQueryParameterAsLastResort() { + Map headers = Map.of(); + Map queryParams = Map.of("token", " token-999 "); + + String token = GatewayJwtResolver.resolveJwtToken(headers, queryParams, objectMapper); + + assertThat(token).isEqualTo("token-999"); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java new file mode 100644 index 00000000..f19f19d8 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java @@ -0,0 +1,159 @@ +package com.zt.plat.module.databus.framework.integration.gateway.security; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO; +import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties; +import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; +import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.http.HttpStatus; + +import java.time.Duration; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class GatewaySecurityFilterTest { + + @Test + void shouldAllowRequestWhenIpPermitted() throws Exception { + ApiGatewayProperties properties = createProperties(); + properties.getSecurity().setEnabled(false); + StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); + ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, new ObjectMapper()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); + request.setRemoteAddr("127.0.0.1"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + + filter.doFilter(request, response, chain); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(chain.getRequest()).isNotNull(); + } + + @Test + void shouldRejectRequestWhenIpDenied() throws Exception { + ApiGatewayProperties properties = createProperties(); + properties.setDeniedIps(Collections.singletonList("10.0.0.1")); + + properties.getSecurity().setEnabled(false); + StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); + ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, new ObjectMapper()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); + request.setRemoteAddr("10.0.0.1"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + + filter.doFilter(request, response, chain); + + assertThat(response.getStatus()).isEqualTo(403); + assertThat(chain.getRequest()).isNull(); + } + + @Test + void shouldValidateSecurityHeadersAndEncryptResponse() throws Exception { + ApiGatewayProperties properties = createProperties(); + StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); + ValueOperations valueOperations = mock(ValueOperations.class); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(Boolean.TRUE); + + ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + ApiClientCredentialDO credential = new ApiClientCredentialDO(); + credential.setAppId("demo-app"); + credential.setSignatureType(null); + credential.setEncryptionKey(null); + credential.setEncryptionType(null); + when(credentialService.findActiveCredential("demo-app")).thenReturn(java.util.Optional.of(credential)); + + properties.getSecurity().setRequireBodyEncryption(false); + properties.getSecurity().setEncryptResponse(false); + + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, new ObjectMapper()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); + request.setRemoteAddr("127.0.0.1"); + long timestamp = System.currentTimeMillis(); + String nonce = UUID.randomUUID().toString().replaceAll("-", ""); + String signature = "d41d8cd98f00b204e9800998ecf8427e"; + request.addHeader(properties.getSecurity().getAppIdHeader(), "demo-app"); + request.addHeader(properties.getSecurity().getTimestampHeader(), String.valueOf(timestamp)); + request.addHeader(properties.getSecurity().getNonceHeader(), nonce); + request.addHeader(properties.getSecurity().getSignatureHeader(), signature); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + + filter.doFilter(request, response, chain); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(chain.getRequest()).isNotNull(); + } + + @Test + void shouldEncryptErrorResponseWhenValidationFails() throws Exception { + ApiGatewayProperties properties = createProperties(); + properties.getSecurity().setRequireBodyEncryption(false); + properties.getSecurity().setEncryptResponse(true); + + StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); + ValueOperations valueOperations = mock(ValueOperations.class); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(Boolean.TRUE); + + ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + ApiClientCredentialDO credential = new ApiClientCredentialDO(); + credential.setAppId("demo-app"); + credential.setEncryptionKey("demo-secret-key"); + credential.setEncryptionType(CryptoSignatureUtils.ENCRYPT_TYPE_AES); + when(credentialService.findActiveCredential("demo-app")).thenReturn(Optional.of(credential)); + + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, new ObjectMapper()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); + request.setRemoteAddr("127.0.0.1"); + long timestamp = System.currentTimeMillis(); + String nonce = UUID.randomUUID().toString().replaceAll("-", ""); + request.addHeader(properties.getSecurity().getAppIdHeader(), "demo-app"); + request.addHeader(properties.getSecurity().getTimestampHeader(), String.valueOf(timestamp)); + request.addHeader(properties.getSecurity().getNonceHeader(), nonce); + request.addHeader(properties.getSecurity().getSignatureHeader(), "invalid-signature"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(request, response, new MockFilterChain()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + String cipherText = response.getContentAsString(); + assertThat(cipherText).isNotBlank(); + assertThat(cipherText.trim()).doesNotStartWith("{"); + + String decrypted = CryptoSignatureUtils.decrypt(cipherText, credential.getEncryptionKey(), credential.getEncryptionType()); + JsonNode node = new ObjectMapper().readTree(decrypted); + assertThat(node.get("code").asInt()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + assertThat(node.get("message").asText()).isEqualTo("签名校验失败"); + } + + private ApiGatewayProperties createProperties() { + ApiGatewayProperties properties = new ApiGatewayProperties(); + properties.setBasePath("/admin-api/databus/api/portal"); + properties.setAllowedIps(Collections.singletonList("127.0.0.1")); + return properties; + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/EndStepHandlerTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/EndStepHandlerTest.java new file mode 100644 index 00000000..68166943 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/EndStepHandlerTest.java @@ -0,0 +1,100 @@ +package com.zt.plat.module.databus.framework.integration.gateway.step.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; +import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionEvaluatorRegistry; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor; +import com.zt.plat.module.databus.framework.integration.gateway.expression.JsonataExpressionEvaluator; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.integration.core.GenericHandler; +import org.springframework.messaging.MessageHeaders; + +import java.util.Collections; +import java.util.Map; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_END_EXECUTION_FAILED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class EndStepHandlerTest { + + private EndStepHandler handler; + private ApiDefinitionAggregate aggregate; + + private static MessageHeaders emptyHeaders() { + return new MessageHeaders(Collections.emptyMap()); + } + + private static ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } + + @BeforeEach + void setUp() { + ExpressionEvaluatorRegistry registry = new ExpressionEvaluatorRegistry(); + registry.register(ExpressionTypeEnum.JSON, new JsonataExpressionEvaluator(createObjectMapper())); + handler = new EndStepHandler(new ExpressionExecutor(registry)); + aggregate = ApiDefinitionAggregate.builder().build(); + } + + @Test + void shouldApplyResponseMappingsToInvocationContext() { + ApiStepDO stepDO = new ApiStepDO(); + stepDO.setId(303L); + stepDO.setType("END"); + stepDO.setResponseMappingExpr("JSON::{\"responseBody\": {\"final\": $.raw}, \"responseStatus\": 201, \"responseMessage\": \"accepted\"}"); + ApiStepDefinition stepDefinition = ApiStepDefinition.builder().step(stepDO).build(); + + ApiInvocationContext context = ApiInvocationContext.create(); + context.setResponseBody(Map.of("raw", Map.of("value", 1))); + context.setResponseStatus(200); + context.setResponseMessage("ok"); + + GenericHandler genericHandler = handler.build(aggregate, stepDefinition); + ApiInvocationContext result = (ApiInvocationContext) genericHandler.handle(context, emptyHeaders()); + + assertThat(result).isSameAs(context); + assertThat(result.getResponseBody()).isInstanceOf(Map.class); + @SuppressWarnings("unchecked") + Map responseBody = (Map) result.getResponseBody(); + assertThat(responseBody).containsKey("final"); + assertThat(result.getResponseStatus()).isEqualTo(201); + assertThat(result.getResponseMessage()).isEqualTo("accepted"); + assertThat(result.getStepResults()).hasSize(1); + assertThat(result.getStepResults().get(0).isSuccess()).isTrue(); + assertThat(result.getStepResults().get(0).getStepId()).isEqualTo(303L); + } + + @Test + void shouldRecordFailureWhenExpressionInvalid() { + ApiStepDO stepDO = new ApiStepDO(); + stepDO.setId(404L); + stepDO.setType("END"); + stepDO.setResponseMappingExpr("JSON::{broken}"); + ApiStepDefinition stepDefinition = ApiStepDefinition.builder().step(stepDO).build(); + + ApiInvocationContext context = ApiInvocationContext.create(); + GenericHandler genericHandler = handler.build(aggregate, stepDefinition); + + assertThatThrownBy(() -> { + genericHandler.handle(context, emptyHeaders()); + }) + .isInstanceOf(ServiceException.class) + .extracting(ex -> ((ServiceException) ex).getCode()) + .isEqualTo(API_STEP_END_EXECUTION_FAILED.getCode()); + assertThat(context.getStepResults()).hasSize(1); + assertThat(context.getStepResults().get(0).isSuccess()).isFalse(); + assertThat(context.getStepResults().get(0).getStepId()).isEqualTo(404L); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandlerTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandlerTest.java new file mode 100644 index 00000000..3c2ce7bf --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandlerTest.java @@ -0,0 +1,99 @@ +package com.zt.plat.module.databus.framework.integration.gateway.step.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.zt.plat.framework.common.exception.ServiceException; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; +import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionEvaluatorRegistry; +import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor; +import com.zt.plat.module.databus.framework.integration.gateway.expression.JsonataExpressionEvaluator; +import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.integration.core.GenericHandler; +import org.springframework.messaging.MessageHeaders; + +import java.util.Collections; +import java.util.Map; + +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_START_EXECUTION_FAILED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class StartStepHandlerTest { + + private StartStepHandler handler; + private ApiDefinitionAggregate aggregate; + + private static MessageHeaders emptyHeaders() { + return new MessageHeaders(Collections.emptyMap()); + } + + private static ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } + + @BeforeEach + void setUp() { + ExpressionEvaluatorRegistry registry = new ExpressionEvaluatorRegistry(); + registry.register(ExpressionTypeEnum.JSON, new JsonataExpressionEvaluator(createObjectMapper())); + handler = new StartStepHandler(new ExpressionExecutor(registry)); + aggregate = ApiDefinitionAggregate.builder().build(); + } + + @Test + void shouldApplyRequestMappingsToInvocationContext() { + ApiStepDO stepDO = new ApiStepDO(); + stepDO.setId(101L); + stepDO.setType("START"); + stepDO.setRequestMappingExpr("JSON::{\"requestHeaders\": {\"x-trace\": \"trace-1\"}, \"body\": {\"wrapped\": $}, \"requestQuery\": {\"flag\": \"Y\"}} "); + ApiStepDefinition stepDefinition = ApiStepDefinition.builder().step(stepDO).build(); + + ApiInvocationContext context = ApiInvocationContext.create(); + context.setRequestBody(Map.of("original", "value")); + context.getRequestHeaders().put("tenant-id", "42"); + + GenericHandler genericHandler = handler.build(aggregate, stepDefinition); + ApiInvocationContext result = (ApiInvocationContext) genericHandler.handle(context, emptyHeaders()); + + assertThat(result).isSameAs(context); + assertThat(result.getRequestHeaders()).containsEntry("x-trace", "trace-1"); + assertThat(result.getRequestQueryParams()).containsEntry("flag", "Y"); + assertThat(result.getRequestBody()).isInstanceOf(Map.class); + @SuppressWarnings("unchecked") + Map mappedBody = (Map) result.getRequestBody(); + assertThat(mappedBody).containsKey("wrapped"); + assertThat(result.getStepResults()).hasSize(1); + assertThat(result.getStepResults().get(0).isSuccess()).isTrue(); + assertThat(result.getStepResults().get(0).getStepId()).isEqualTo(101L); + } + + @Test + void shouldRecordFailureWhenExpressionInvalid() { + ApiStepDO stepDO = new ApiStepDO(); + stepDO.setId(202L); + stepDO.setType("START"); + stepDO.setRequestMappingExpr("JSON::{invalid}"); + ApiStepDefinition stepDefinition = ApiStepDefinition.builder().step(stepDO).build(); + + ApiInvocationContext context = ApiInvocationContext.create(); + GenericHandler genericHandler = handler.build(aggregate, stepDefinition); + + assertThatThrownBy(() -> { + genericHandler.handle(context, emptyHeaders()); + }) + .isInstanceOf(ServiceException.class) + .extracting(ex -> ((ServiceException) ex).getCode()) + .isEqualTo(API_STEP_START_EXECUTION_FAILED.getCode()); + assertThat(context.getStepResults()).hasSize(1); + assertThat(context.getStepResults().get(0).isSuccess()).isFalse(); + assertThat(context.getStepResults().get(0).getStepId()).isEqualTo(202L); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionServiceImplTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionServiceImplTest.java index cfa8bf38..004baf8f 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionServiceImplTest.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionServiceImplTest.java @@ -8,12 +8,10 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefi import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionStepSaveReqVO; import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionTransformSaveReqVO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; -import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO; import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper; -import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyAuthMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyRateLimitMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper; @@ -60,8 +58,6 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { @Resource private ApiTransformMapper apiTransformMapper; @Resource - private ApiPolicyAuthMapper apiPolicyAuthMapper; - @Resource private ApiPolicyRateLimitMapper apiPolicyRateLimitMapper; @MockBean @@ -84,9 +80,8 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { @Test void testCreate_success() { TenantContextHolder.setTenantId(1L); - Long authId = insertAuthPolicy(); Long rateId = insertRateLimitPolicy(); - ApiDefinitionSaveReqVO reqVO = buildSaveReq(null, authId, rateId); + ApiDefinitionSaveReqVO reqVO = buildSaveReq(null, rateId); Long definitionId = apiDefinitionService.create(reqVO); ApiDefinitionDO definition = apiDefinitionMapper.selectById(definitionId); @@ -98,16 +93,18 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { assertEquals(reqVO.getDescription(), definition.getDescription()); List steps = apiStepMapper.selectByApiId(definitionId); - assertEquals(1, steps.size()); - ApiStepDO step = steps.get(0); - assertEquals(1, step.getStepOrder()); - assertEquals("HTTP", step.getType()); + assertEquals(3, steps.size()); + assertEquals("START", steps.get(0).getType()); + assertEquals(Integer.valueOf(1), steps.get(0).getStepOrder()); + assertEquals("HTTP", steps.get(1).getType()); + assertEquals(Integer.valueOf(2), steps.get(1).getStepOrder()); + assertEquals("END", steps.get(2).getType()); + assertEquals(Integer.valueOf(3), steps.get(2).getStepOrder()); List apiLevelTransforms = apiTransformMapper.selectApiLevelTransforms(definitionId); - assertEquals(1, apiLevelTransforms.size()); - assertEquals("REQUEST_PRE", apiLevelTransforms.get(0).getPhase()); + assertEquals(0, apiLevelTransforms.size()); - List stepTransforms = apiTransformMapper.selectByStepId(step.getId()); + List stepTransforms = apiTransformMapper.selectByStepId(steps.get(1).getId()); assertEquals(1, stepTransforms.size()); assertEquals("RESPONSE_PRE", stepTransforms.get(0).getPhase()); } @@ -121,11 +118,10 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { definition.setApiCode("order.create"); definition.setVersion("v1"); definition.setHttpMethod("POST"); - definition.setUriPattern("/order/create"); definition.setStatus(ApiStatusEnum.ONLINE.getStatus()); apiDefinitionMapper.insert(definition); - ApiDefinitionSaveReqVO reqVO = buildSaveReq(null, null, null); + ApiDefinitionSaveReqVO reqVO = buildSaveReq(null, null); ServiceException exception = assertThrows(ServiceException.class, () -> apiDefinitionService.create(reqVO)); assertEquals(GatewayServiceErrorCodeConstants.API_DEFINITION_DUPLICATE.getCode(), exception.getCode()); @@ -134,7 +130,6 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { @Test void testUpdate_replaceSteps() { TenantContextHolder.setTenantId(1L); - Long authId = insertAuthPolicy(); Long rateId = insertRateLimitPolicy(); ApiDefinitionDO definition = new ApiDefinitionDO(); definition.setTenantId(1L); @@ -142,7 +137,6 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { definition.setApiCode("order.update"); definition.setVersion("v1"); definition.setHttpMethod("POST"); - definition.setUriPattern("/order/update"); definition.setStatus(ApiStatusEnum.ONLINE.getStatus()); apiDefinitionMapper.insert(definition); @@ -164,20 +158,21 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { oldTransform.setDeleted(false); apiTransformMapper.insert(oldTransform); - ApiDefinitionSaveReqVO reqVO = buildSaveReq(definition.getId(), authId, rateId); + ApiDefinitionSaveReqVO reqVO = buildSaveReq(definition.getId(), rateId); reqVO.setApiCode("order.update"); reqVO.setVersion("v2"); - reqVO.getSteps().get(0).setStepOrder(2); apiDefinitionService.update(reqVO); List steps = apiStepMapper.selectByApiId(definition.getId()); - assertEquals(1, steps.size()); - assertEquals(2, steps.get(0).getStepOrder()); + assertEquals(3, steps.size()); + assertEquals("START", steps.get(0).getType()); + assertEquals("HTTP", steps.get(1).getType()); + assertEquals("END", steps.get(2).getType()); List transforms = apiTransformMapper.selectByApiId(definition.getId()); assertThat(transforms) .extracting(ApiTransformDO::getPhase) - .containsExactlyInAnyOrder("REQUEST_PRE", "RESPONSE_PRE"); + .containsExactlyInAnyOrder("RESPONSE_PRE"); } @Test @@ -189,7 +184,6 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { definition.setApiCode("order.delete"); definition.setVersion("v1"); definition.setHttpMethod("DELETE"); - definition.setUriPattern("/order/delete"); definition.setStatus(ApiStatusEnum.ONLINE.getStatus()); apiDefinitionMapper.insert(definition); @@ -219,26 +213,24 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { assertThat(apiTransformMapper.selectByApiId(definition.getId())).isEmpty(); } - private ApiDefinitionSaveReqVO buildSaveReq(Long id, Long authId, Long rateId) { + private ApiDefinitionSaveReqVO buildSaveReq(Long id, Long rateId) { ApiDefinitionSaveReqVO reqVO = new ApiDefinitionSaveReqVO(); reqVO.setId(id); reqVO.setApiCode("order.create"); reqVO.setVersion("v1"); reqVO.setHttpMethod("POST"); - reqVO.setUriPattern("/order/create"); reqVO.setStatus(ApiStatusEnum.ONLINE.getStatus()); reqVO.setDescription("create order"); - reqVO.setAuthPolicyId(authId); reqVO.setRateLimitId(rateId); - ApiDefinitionTransformSaveReqVO apiTransform = new ApiDefinitionTransformSaveReqVO(); - apiTransform.setPhase("REQUEST_PRE"); - apiTransform.setExpressionType("JSON"); - apiTransform.setExpression("{}"); - reqVO.getApiLevelTransforms().add(apiTransform); + ApiDefinitionStepSaveReqVO start = new ApiDefinitionStepSaveReqVO(); + start.setStepOrder(1); + start.setType("START"); + start.setRequestMappingExpr("JSON::{}"); + reqVO.getSteps().add(start); ApiDefinitionStepSaveReqVO step = new ApiDefinitionStepSaveReqVO(); - step.setStepOrder(1); + step.setStepOrder(2); step.setType("HTTP"); step.setTargetEndpoint("https://api.example.com/order"); @@ -247,20 +239,14 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { stepTransform.setExpressionType("JSON"); stepTransform.setExpression("{}"); step.getTransforms().add(stepTransform); - reqVO.getSteps().add(step); - return reqVO; - } - private Long insertAuthPolicy() { - ApiPolicyAuthDO policy = new ApiPolicyAuthDO(); - policy.setName("auth"); - policy.setType("BASIC"); - policy.setConfig("{}"); - policy.setTenantId(1L); - policy.setDeleted(false); - apiPolicyAuthMapper.insert(policy); - return policy.getId(); + ApiDefinitionStepSaveReqVO end = new ApiDefinitionStepSaveReqVO(); + end.setStepOrder(3); + end.setType("END"); + end.setResponseMappingExpr("JSON::{}"); + reqVO.getSteps().add(end); + return reqVO; } private Long insertRateLimitPolicy() { diff --git a/zt-module-databus/zt-module-databus-server/src/test/resources/sql/clean.sql b/zt-module-databus/zt-module-databus-server/src/test/resources/sql/clean.sql index 43823faa..f17ff31f 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/resources/sql/clean.sql +++ b/zt-module-databus/zt-module-databus-server/src/test/resources/sql/clean.sql @@ -1,7 +1,6 @@ DELETE FROM "databus_api_transform"; DELETE FROM "databus_api_step"; DELETE FROM "databus_api_definition"; -DELETE FROM "databus_policy_auth"; DELETE FROM "databus_policy_rate_limit"; DELETE FROM "databus_policy_audit"; DELETE FROM "databus_api_flow_publish"; diff --git a/zt-module-databus/zt-module-databus-server/src/test/resources/sql/create_tables.sql b/zt-module-databus/zt-module-databus-server/src/test/resources/sql/create_tables.sql index 3427a929..d337c2f7 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/resources/sql/create_tables.sql +++ b/zt-module-databus/zt-module-databus-server/src/test/resources/sql/create_tables.sql @@ -1,18 +1,14 @@ CREATE TABLE IF NOT EXISTS databus_api_definition ( id BIGINT PRIMARY KEY, api_code VARCHAR(255) NOT NULL, - uri_pattern VARCHAR(512), http_method VARCHAR(16), version VARCHAR(64), status INT, description VARCHAR(1024), - auth_policy_id BIGINT, rate_limit_id BIGINT, audit_policy_id BIGINT, response_template CLOB, - cache_strategy VARCHAR(255), updated_at TIMESTAMP, - grey_released BOOLEAN, tenant_id BIGINT, create_time TIMESTAMP, update_time TIMESTAMP, @@ -32,7 +28,6 @@ CREATE TABLE IF NOT EXISTS databus_api_step ( response_mapping_expr VARCHAR(1024), transform_id BIGINT, timeout BIGINT, - retry_strategy VARCHAR(255), fallback_strategy VARCHAR(255), condition_expr VARCHAR(1024), stop_on_error BOOLEAN, @@ -60,20 +55,6 @@ CREATE TABLE IF NOT EXISTS databus_api_transform ( deleted BOOLEAN ); -CREATE TABLE IF NOT EXISTS databus_policy_auth ( - id BIGINT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - type VARCHAR(64), - config CLOB, - description VARCHAR(512), - tenant_id BIGINT, - create_time TIMESTAMP, - update_time TIMESTAMP, - creator VARCHAR(64), - updater VARCHAR(64), - deleted BOOLEAN -); - CREATE TABLE IF NOT EXISTS databus_policy_rate_limit ( id BIGINT PRIMARY KEY, name VARCHAR(255) NOT NULL, diff --git a/zt-module-html2pdf/zt-module-html2pdf-server/src/main/resources/application.yml b/zt-module-html2pdf/zt-module-html2pdf-server/src/main/resources/application.yml index 75d0c588..1504c97b 100644 --- a/zt-module-html2pdf/zt-module-html2pdf-server/src/main/resources/application.yml +++ b/zt-module-html2pdf/zt-module-html2pdf-server/src/main/resources/application.yml @@ -114,7 +114,7 @@ zt: - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 swagger: - title: 管理后台 + title: html转pdf能力 description: 提供管理员管理的所有功能 version: ${zt.info.version} tenant: # 多租户相关配置项 diff --git a/zt-module-infra/zt-module-infra-server/src/main/resources/application.yaml b/zt-module-infra/zt-module-infra-server/src/main/resources/application.yaml index 83138902..1d4dd83d 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/resources/application.yaml +++ b/zt-module-infra/zt-module-infra-server/src/main/resources/application.yaml @@ -173,8 +173,8 @@ zt: topic: ${spring.application.name}-websocket # 消息发送的 Kafka Topic consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 Kafka Consumer Group swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: 基础设施 + description: 提供基础设施功能 version: ${zt.info.version} codegen: base-package: com.zt.plat diff --git a/zt-module-iot/zt-module-iot-biz/src/main/resources/application.yaml b/zt-module-iot/zt-module-iot-biz/src/main/resources/application.yaml index 7683c123..da0b184d 100644 --- a/zt-module-iot/zt-module-iot-biz/src/main/resources/application.yaml +++ b/zt-module-iot/zt-module-iot-biz/src/main/resources/application.yaml @@ -140,8 +140,8 @@ zt: exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: IOT能力 + description: 提供 IOT 能力 version: ${zt.info.version} tenant: # 多租户相关配置项 enable: true diff --git a/zt-module-mp/zt-module-mp-server/src/main/resources/application.yaml b/zt-module-mp/zt-module-mp-server/src/main/resources/application.yaml index 3126ea16..6a650d0b 100644 --- a/zt-module-mp/zt-module-mp-server/src/main/resources/application.yaml +++ b/zt-module-mp/zt-module-mp-server/src/main/resources/application.yaml @@ -134,8 +134,8 @@ zt: permit-all_urls: - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: 小程序 + description: 提供小程序功能 version: ${zt.info.version} tenant: # 多租户相关配置项 enable: true diff --git a/zt-module-report/zt-module-report-server/src/main/resources/application.yaml b/zt-module-report/zt-module-report-server/src/main/resources/application.yaml index ecb59dc6..c872752d 100644 --- a/zt-module-report/zt-module-report-server/src/main/resources/application.yaml +++ b/zt-module-report/zt-module-report-server/src/main/resources/application.yaml @@ -121,8 +121,8 @@ zt: exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: 报表 + description: 提供报表功能 version: ${zt.info.version} tenant: # 多租户相关配置项 enable: true diff --git a/zt-module-rule/zt-module-rule-server/src/main/resources/application.yml b/zt-module-rule/zt-module-rule-server/src/main/resources/application.yml index 61443a57..8a7ec1fd 100644 --- a/zt-module-rule/zt-module-rule-server/src/main/resources/application.yml +++ b/zt-module-rule/zt-module-rule-server/src/main/resources/application.yml @@ -114,8 +114,8 @@ zt: - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: 管规则引擎 + description: 提供规则引擎功能 version: ${zt.info.version} tenant: # 多租户相关配置项 enable: true diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java index acf24e46..176097fb 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java @@ -55,6 +55,15 @@ public class DeptController { return success(true); } + @PostMapping("init-codes") + @Operation(summary = "初始化部门编码", description = "按照层级自动为全部部门重新生成编码") + @PreAuthorize("@ss.hasPermission('system:dept:init-code')") + @TenantIgnore + public CommonResult initializeDeptCodes() { + deptService.initializeDeptCodes(); + return success(true); + } + @DeleteMapping("delete") @Operation(summary = "删除部门") @Parameter(name = "id", description = "编号", required = true, example = "1024") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptCodeInitReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptCodeInitReqVO.java new file mode 100644 index 00000000..e69de29b diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java index 0dc84c42..8d5fa316 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptSaveReqVO.java @@ -19,7 +19,7 @@ public class DeptSaveReqVO { @Schema(description = "部门编号", example = "1024") private Long id; - @Schema(description = "部门编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "DEPT_001") + @Schema(description = "部门编码", example = "ZT001001") @Size(max = 50, message = "部门编码长度不能超过 50 个字符") private String code; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sync/SyncController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sync/SyncController.java index 6a89f9ef..d778b166 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sync/SyncController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sync/SyncController.java @@ -2,6 +2,7 @@ package com.zt.plat.module.system.controller.admin.sync; import com.zt.plat.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; import com.zt.plat.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; import com.zt.plat.framework.security.core.LoginUser; import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.framework.tenant.core.aop.TenantIgnore; @@ -558,7 +559,7 @@ public class SyncController { syncLogService.logDecryptResult(logId, bimRequestId, bodyJson, authUser, true); // 签名验证 - boolean signatureValid = SyncVerifyUtil.verifySignature(map, "MD5"); + boolean signatureValid = CryptoSignatureUtils.verifySignature(map, CryptoSignatureUtils.SIGNATURE_TYPE_MD5); syncLogService.logSignatureVerifyResult(logId, signatureValid); if (!signatureValid) { throw exception(SYNC_SIGNATURE_VERIFY_FAILED); @@ -608,8 +609,8 @@ public class SyncController { String bodyJson; String jsonString = JSON.toJSONString(object); try { - bodyJson = SyncVerifyUtil.encrypt(jsonString, encryptKey, "AES"); - } catch (Exception e) { + bodyJson = CryptoSignatureUtils.encrypt(jsonString, encryptKey, CryptoSignatureUtils.ENCRYPT_TYPE_AES); + } catch (IllegalArgumentException | IllegalStateException e) { throw exception(SYNC_DECRYPT_TYPE); } return bodyJson; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java index e3fa2194..04f841a3 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java @@ -5,6 +5,7 @@ import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; @@ -88,6 +89,20 @@ public interface DeptMapper extends BaseMapperX { ); } + /** + * 查询指定父部门下编码最大的子部门 + * + * @param parentId 父部门ID + * @return 编码最大的子部门 + */ + default DeptDO selectLastChildByCode(Long parentId) { + return selectOne(new LambdaQueryWrapper() + .eq(DeptDO::getParentId, parentId) + .isNotNull(DeptDO::getCode) + .orderByDesc(DeptDO::getCode) + .last("LIMIT 1")); + } + /** * 根据部门编码查询部门 * diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java index 25924e7e..2181426e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java @@ -152,4 +152,9 @@ public interface DeptService { * @return 公司列表 */ List getAllCompanyList(); + + /** + * 按照新的编码规则初始化全部部门编码 + */ + void initializeDeptCodes(); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java index 148dc3e1..918fc182 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java @@ -8,6 +8,7 @@ import com.zt.plat.framework.common.enums.CommonStatusEnum; import com.zt.plat.framework.common.pojo.CompanyDeptInfo; import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.framework.datapermission.core.annotation.DataPermission; +import com.zt.plat.framework.tenant.core.aop.TenantIgnore; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; @@ -21,9 +22,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -47,6 +50,13 @@ public class DeptServiceImpl implements DeptService { @Resource private UserDeptMapper userDeptMapper; + private static final String ROOT_CODE_PREFIX = "ZT"; + private static final int CODE_SEGMENT_LENGTH = 3; + private static final int MAX_SEQUENCE = 999; + private static final Comparator DEPT_COMPARATOR = Comparator + .comparing(DeptDO::getSort, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(DeptDO::getId, Comparator.nullsLast(Comparator.naturalOrder())); + @Override @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 @@ -60,8 +70,10 @@ public class DeptServiceImpl implements DeptService { validateParentDept(null, createReqVO.getParentId()); // 校验部门名的唯一性 validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); - // 校验部门编码的唯一性 - validateDeptCodeUnique(null, createReqVO.getCode()); + // 生成并校验部门编码 + String generatedCode = generateDeptCode(createReqVO.getParentId()); + createReqVO.setCode(generatedCode); + validateDeptCodeUnique(null, generatedCode); // 插入部门 DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class); @@ -82,15 +94,29 @@ public class DeptServiceImpl implements DeptService { updateReqVO.setParentId(DeptDO.PARENT_ID_ROOT); } // 校验自己存在 - validateDeptExists(updateReqVO.getId()); + DeptDO originalDept = getRequiredDept(updateReqVO.getId()); // 校验父部门的有效性 validateParentDept(updateReqVO.getId(), updateReqVO.getParentId()); // 校验部门名的唯一性 validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); + // 如果上级发生变化,需要重新生成编码并同步子级 + Long newParentId = normalizeParentId(updateReqVO.getParentId()); + Long oldParentId = normalizeParentId(originalDept.getParentId()); + boolean parentChanged = !Objects.equals(newParentId, oldParentId); + if (parentChanged) { + String newCode = generateDeptCode(updateReqVO.getParentId()); + updateReqVO.setCode(newCode); + } else { + updateReqVO.setCode(originalDept.getCode()); + } // 更新部门 DeptDO updateObj = BeanUtils.toBean(updateReqVO, DeptDO.class); deptMapper.updateById(updateObj); + + if (parentChanged) { + refreshChildCodesRecursively(updateObj.getId(), updateReqVO.getCode()); + } } @Override @@ -112,10 +138,18 @@ public class DeptServiceImpl implements DeptService { if (id == null) { return; } + getRequiredDept(id); + } + + private DeptDO getRequiredDept(Long id) { + if (id == null) { + throw exception(DEPT_NOT_FOUND); + } DeptDO dept = deptMapper.selectById(id); if (dept == null) { throw exception(DEPT_NOT_FOUND); } + return dept; } @VisibleForTesting @@ -186,6 +220,98 @@ public class DeptServiceImpl implements DeptService { } } + private String generateDeptCode(Long parentId) { + Long effectiveParentId = normalizeParentId(parentId); + String prefix = ROOT_CODE_PREFIX; + if (!DeptDO.PARENT_ID_ROOT.equals(effectiveParentId)) { + DeptDO parentDept = deptMapper.selectById(effectiveParentId); + if (parentDept == null) { + throw exception(DEPT_PARENT_NOT_EXITS); + } + if (StrUtil.isBlank(parentDept.getCode())) { + throw exception(DEPT_PARENT_CODE_NOT_INITIALIZED); + } + prefix = parentDept.getCode(); + } + + int nextSequence = determineNextSequence(effectiveParentId, prefix); + assertSequenceRange(nextSequence); + return prefix + formatSequence(nextSequence); + } + + private int determineNextSequence(Long parentId, String prefix) { + DeptDO lastChild = deptMapper.selectLastChildByCode(parentId); + Integer sequence = parseSequence(lastChild != null ? lastChild.getCode() : null, prefix); + if (sequence != null) { + return sequence + 1; + } + return deptMapper.selectListByParentId(parentId, null).stream() + .map(DeptDO::getCode) + .map(code -> parseSequence(code, prefix)) + .filter(Objects::nonNull) + .max(Integer::compareTo) + .map(val -> val + 1) + .orElse(1); + } + + private Integer parseSequence(String code, String prefix) { + if (StrUtil.isBlank(code) || StrUtil.isBlank(prefix) || !code.startsWith(prefix)) { + return null; + } + String suffix = code.substring(prefix.length()); + if (suffix.length() != CODE_SEGMENT_LENGTH || !StrUtil.isNumeric(suffix)) { + return null; + } + return Integer.parseInt(suffix); + } + + private void refreshChildCodesRecursively(Long parentId, String parentCode) { + rebuildCodes(parentId, parentCode, id -> deptMapper.selectListByParentId(id, null)); + } + + private void rebuildCodes(Long parentId, String parentCode, Function> childrenSupplier) { + List children = sortChildren(childrenSupplier.apply(parentId)); + if (CollUtil.isEmpty(children)) { + return; + } + int sequence = 1; + for (DeptDO child : children) { + assertSequenceRange(sequence); + String childCode = parentCode + formatSequence(sequence++); + updateDeptCode(child.getId(), childCode); + rebuildCodes(child.getId(), childCode, childrenSupplier); + } + } + + private List sortChildren(List children) { + if (CollUtil.isEmpty(children)) { + return Collections.emptyList(); + } + children.sort(DEPT_COMPARATOR); + return children; + } + + private void assertSequenceRange(int sequence) { + if (sequence > MAX_SEQUENCE) { + throw exception(DEPT_CODE_OUT_OF_RANGE); + } + } + + private String formatSequence(int sequence) { + return StrUtil.padPre(String.valueOf(sequence), CODE_SEGMENT_LENGTH, '0'); + } + + private Long normalizeParentId(Long parentId) { + return parentId == null ? DeptDO.PARENT_ID_ROOT : parentId; + } + + private void updateDeptCode(Long deptId, String code) { + DeptDO update = new DeptDO(); + update.setId(deptId); + update.setCode(code); + deptMapper.updateById(update); + } + @Override public DeptDO getDept(Long id) { return deptMapper.selectById(id); @@ -465,4 +591,27 @@ public class DeptServiceImpl implements DeptService { return deptMapper.selectAllCompanyList(CommonStatusEnum.ENABLE.getStatus()); } + @Override + @TenantIgnore + @Transactional(rollbackFor = Exception.class) + public void initializeDeptCodes() { + List allDepts = deptMapper.selectList(); + if (CollUtil.isEmpty(allDepts)) { + return; + } + + Map> childrenMap = allDepts.stream() + .collect(Collectors.groupingBy(dept -> normalizeParentId(dept.getParentId()))); + Function> childrenSupplier = id -> new ArrayList<>(childrenMap.getOrDefault(id, Collections.emptyList())); + List rootDepts = sortChildren(childrenSupplier.apply(DeptDO.PARENT_ID_ROOT)); + + int sequence = 1; + for (DeptDO root : rootDepts) { + assertSequenceRange(sequence); + String code = ROOT_CODE_PREFIX + formatSequence(sequence++); + updateDeptCode(root.getId(), code); + rebuildCodes(root.getId(), code, childrenSupplier); + } + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2Service.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2Service.java index de533eab..9a278c78 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2Service.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2Service.java @@ -37,6 +37,7 @@ public interface EbanOAuth2Service { private String mobile; private String deptName; private String uid; + private String displayName; private String rawUserInfoJson; private EbanOAuth2ServiceImpl.EbanTokenInfo tokenInfo; // 添加Token信息 @@ -70,6 +71,9 @@ public interface EbanOAuth2Service { public String getUid() { return uid; } public void setUid(String uid) { this.uid = uid; } + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getRawUserInfoJson() { return rawUserInfoJson; } public void setRawUserInfoJson(String rawUserInfoJson) { this.rawUserInfoJson = rawUserInfoJson; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2ServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2ServiceImpl.java index 6a148f3b..41e3c8c9 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2ServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2ServiceImpl.java @@ -77,15 +77,16 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID); } - String uid = userInfo.getUid(); - if (StrUtil.isBlank(uid)) { + String displayName = StrUtil.trim(StrUtil.blankToDefault(userInfo.getDisplayName(), userInfo.getUsername())); + if (StrUtil.isBlank(displayName)) { + log.error("E办OAuth2用户信息缺少displayName与loginName,无法匹配账号: {}", JSONUtil.toJsonStr(userInfo)); throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID); } - Long userId = parseUid(uid); - AdminUserDO user = userService.getUser(userId); + AdminUserDO user = userService.getUserByUsername(displayName); if (user == null) { - createLoginLog(null, uid, LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.BAD_CREDENTIALS); + createLoginLog(null, displayName, LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.BAD_CREDENTIALS); + log.warn("E办OAuth2用户displayName未在系统中找到对应账号: {}", displayName); throw exception(AUTH_LOGIN_EBAN_USER_NOT_SYNC); } @@ -105,13 +106,13 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { tokenInfo.getAccessToken(), tokenInfo.getRefreshToken(), tokenInfo.getExpiresIn(), - uid, + userInfo.getUid(), userInfoJson ); - log.info("成功保存E办token,userId={}, uid={}", user.getId(), uid); + log.info("成功保存E办token,userId={}, uid={}, displayName={}", user.getId(), userInfo.getUid(), displayName); } } catch (Exception e) { - log.error("保存E办token失败,userId={}, uid={}", user.getId(), uid, e); + log.error("保存E办token失败,userId={}, uid={}, displayName={}", user.getId(), userInfo.getUid(), displayName, e); } return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); @@ -132,21 +133,16 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { if (StrUtil.isBlank(userInfo.getUid()) && StrUtil.isNotBlank(tokenInfo.getUid())) { userInfo.setUid(tokenInfo.getUid()); } - return userInfo; - } - - private Long parseUid(String uid) { - try { - return Long.parseLong(uid); - } catch (NumberFormatException ex) { - log.warn("E办uid无法解析: {}", uid); - throw exception(AUTH_LOGIN_EBAN_USER_NOT_SYNC); + if (StrUtil.isBlank(userInfo.getDisplayName()) && StrUtil.isNotBlank(tokenInfo.getDisplayName())) { + userInfo.setDisplayName(StrUtil.trim(tokenInfo.getDisplayName())); } + return userInfo; } private String buildBasicUserInfoJson(EbanUserInfo userInfo) { JSONObject jsonObject = new JSONObject(); jsonObject.put("uid", userInfo.getUid()); + jsonObject.put("displayName", userInfo.getDisplayName()); jsonObject.put("loginName", userInfo.getUsername()); jsonObject.put("realName", userInfo.getRealName()); jsonObject.put("email", userInfo.getEmail()); @@ -164,6 +160,7 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { private Integer expiresIn; private String uid; private String createDate; + private String displayName; // 构造函数和getter/setter方法 public EbanTokenInfo() {} @@ -190,6 +187,9 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { public String getCreateDate() { return createDate; } public void setCreateDate(String createDate) { this.createDate = createDate; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } } /** @@ -231,10 +231,13 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { Integer expiresIn = jsonResponse.getInt("expires_in"); String uid = jsonResponse.getStr("uid"); String createDate = jsonResponse.getStr("createDate"); + String displayName = jsonResponse.getStr("displayName"); - log.info("成功获取E办access_token,uid={}, expires_in={}", uid, expiresIn); + log.info("成功获取E办access_token,uid={}, displayName={}, expires_in={}", uid, displayName, expiresIn); - return new EbanTokenInfo(accessToken, refreshToken, expiresIn, uid, createDate); + EbanTokenInfo tokenInfo = new EbanTokenInfo(accessToken, refreshToken, expiresIn, uid, createDate); + tokenInfo.setDisplayName(displayName); + return tokenInfo; } /** @@ -277,6 +280,7 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { userInfo.setMobile(userJson.getStr("mobile")); userInfo.setDeptName(userJson.getStr("deptName")); userInfo.setUid(userJson.getStr("uid")); + userInfo.setDisplayName(resolveDisplayName(userJson)); userInfo.setRawUserInfoJson(userJson.toString()); if (StrUtil.isBlank(userInfo.getRealName()) && userJson.containsKey("spRoleList")) { @@ -296,6 +300,23 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { return userInfo; } + + private String resolveDisplayName(JSONObject userJson) { + String displayName = userJson.getStr("displayName"); + if (StrUtil.isBlank(displayName)) { + displayName = userJson.getStr("display_name"); + } + if (StrUtil.isBlank(displayName)) { + displayName = userJson.getStr("displayname"); + } + if (StrUtil.isBlank(displayName)) { + displayName = userJson.getStr("loginName"); + } + if (StrUtil.isBlank(displayName)) { + displayName = userJson.getStr("realName"); + } + return StrUtil.trim(displayName); + } private JSONObject parseResponseBody(String body, String action) { try { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/util/sync/SyncVerifyUtil.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/util/sync/SyncVerifyUtil.java index d4ad638e..0917bb9a 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/util/sync/SyncVerifyUtil.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/util/sync/SyncVerifyUtil.java @@ -1,22 +1,9 @@ package com.zt.plat.module.system.util.sync; -import cn.hutool.crypto.SecureUtil; -import cn.hutool.crypto.symmetric.DES; +import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; import com.zt.plat.module.system.enums.common.SexEnum; -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Base64; -import java.util.Map; -import java.util.TreeMap; - import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.zt.plat.module.system.enums.ErrorCodeConstants.AUTH_LOGIN_BAD_CREDENTIALS; import static com.zt.plat.module.system.enums.ErrorCodeConstants.SYNC_DECRYPT_TYPE; /** @@ -25,105 +12,13 @@ import static com.zt.plat.module.system.enums.ErrorCodeConstants.SYNC_DECRYPT_TY public class SyncVerifyUtil { public static String decrypt(String ciphertext, String key, String type) { - if ("AES".equalsIgnoreCase(type)) { - try { - Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key)); - byte[] result = cipher.doFinal(Base64.getDecoder().decode(ciphertext.getBytes())); - return new String(result, StandardCharsets.UTF_8); - } catch (Exception e) { - throw exception(SYNC_DECRYPT_TYPE); - } - } else if ("DES".equalsIgnoreCase(type)) { - byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); - byte[] desKey = new byte[8]; - System.arraycopy(keyBytes, 0, desKey, 0, Math.min(keyBytes.length, desKey.length)); - DES des = SecureUtil.des(desKey); - - byte[] encryptedBytes = Base64.getDecoder().decode(ciphertext); - return new String(des.decrypt(encryptedBytes), StandardCharsets.UTF_8); - } else { + try { + return CryptoSignatureUtils.decrypt(ciphertext, key, type); + } catch (IllegalArgumentException | IllegalStateException ex) { throw exception(SYNC_DECRYPT_TYPE); } } - /** - * 生成与原始代码兼容的密钥 - */ - private static SecretKeySpec getSecretKey(String password) { - try { - KeyGenerator kg = KeyGenerator.getInstance("AES"); - SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); - random.setSeed(password.getBytes()); - kg.init(128, random); - SecretKey secretKey = kg.generateKey(); - return new SecretKeySpec(secretKey.getEncoded(), "AES"); - } catch (NoSuchAlgorithmException ex) { - throw exception(AUTH_LOGIN_BAD_CREDENTIALS); - } - } - - /** - * 对称加密(Base64 格式输出) - * @param plaintext 明文内容 - * @param key 密钥 - * @param type 加密类型,支持 AES、DES - * @return 密文(Base64 格式) - */ - public static String encrypt(String plaintext, String key, String type) { - if ("AES".equalsIgnoreCase(type)) { - try { - Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); - byte[] byteContent = plaintext.getBytes(StandardCharsets.UTF_8); - cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key)); - byte[] result = cipher.doFinal(byteContent); - return Base64.getEncoder().encodeToString(result); - } catch (Exception e) { - throw exception(AUTH_LOGIN_BAD_CREDENTIALS); - } - } else if ("DES".equalsIgnoreCase(type)) { - byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); - byte[] desKey = new byte[8]; - System.arraycopy(keyBytes, 0, desKey, 0, Math.min(keyBytes.length, desKey.length)); - DES des = SecureUtil.des(desKey); - byte[] encrypted = des.encrypt(plaintext.getBytes(StandardCharsets.UTF_8)); - return Base64.getEncoder().encodeToString(encrypted); - } else { - throw exception(AUTH_LOGIN_BAD_CREDENTIALS); - } - } - - public static boolean verifySignature(Map reqMap, String type) { - // 排序并拼接参数,忽略 signature 字段 - Map sortedMap = new TreeMap<>(reqMap); - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : sortedMap.entrySet()) { - String key = entry.getKey(); - if ("signature".equals(key) || entry.getValue() == null) { - continue; - } - sb.append(key).append("=").append(entry.getValue()).append("&"); - } - if (!sb.isEmpty()) { - sb.deleteCharAt(sb.length() - 1); - } - // 取出请求中的 signature - String provided = (String) reqMap.get("signature"); - if (provided == null) { - return false; - } - // 计算签名 - String computed; - if ("MD5".equalsIgnoreCase(type)) { - computed = SecureUtil.md5(sb.toString()); - } else if ("SHA256".equalsIgnoreCase(type)) { - computed = SecureUtil.sha256(sb.toString()); - } else { - throw exception(AUTH_LOGIN_BAD_CREDENTIALS); - } - return provided.equalsIgnoreCase(computed); - } - /** * e 办性别编码转换为内部性别编码 * 外部:女=0,男=1 diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java index cb4f3843..e0fd57ef 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java @@ -35,6 +35,18 @@ public class DeptServiceImplTest extends BaseDbUnitTest { @Resource private DeptMapper deptMapper; + private Long createDept(Long parentId, String name, int sort) { + DeptSaveReqVO reqVO = new DeptSaveReqVO(); + reqVO.setParentId(parentId); + reqVO.setName(name); + reqVO.setSort(sort); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setDeptSource(1); + reqVO.setIsCompany(false); + reqVO.setIsGroup(false); + return deptService.createDept(reqVO); + } + @Test public void testCreateDept() { // 准备参数 @@ -42,6 +54,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest { o.setId(null); // 防止 id 被设置 o.setParentId(DeptDO.PARENT_ID_ROOT); o.setStatus(randomCommonStatus()); + o.setCode(null); }).setDeptSource(1); // 调用 @@ -50,13 +63,35 @@ public class DeptServiceImplTest extends BaseDbUnitTest { assertNotNull(deptId); // 校验记录的属性是否正确 DeptDO deptDO = deptMapper.selectById(deptId); - assertPojoEquals(reqVO, deptDO, "id"); + assertPojoEquals(reqVO, deptDO, "id", "code"); + assertEquals("ZT001", deptDO.getCode()); + } + + @Test + public void testCreateDept_childCodeGeneration() { + Long parentId = createDept(DeptDO.PARENT_ID_ROOT, "总部", 1); + DeptDO parentDept = deptMapper.selectById(parentId); + + DeptSaveReqVO childReq = new DeptSaveReqVO(); + childReq.setParentId(parentId); + childReq.setName("事业部"); + childReq.setSort(1); + childReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + childReq.setDeptSource(1); + Long childId = deptService.createDept(childReq); + + DeptDO childDept = deptMapper.selectById(childId); + assertEquals(parentDept.getCode() + "001", childDept.getCode()); } @Test public void testUpdateDept() { // mock 数据 - DeptDO dbDeptDO = randomPojo(DeptDO.class, o -> o.setStatus(randomCommonStatus())).setDeptSource(null); + DeptDO dbDeptDO = randomPojo(DeptDO.class, o -> { + o.setStatus(randomCommonStatus()); + o.setParentId(DeptDO.PARENT_ID_ROOT); + }).setDeptSource(null); + dbDeptDO.setCode("ZT001"); deptMapper.insert(dbDeptDO);// @Sql: 先插入出一条存在的数据 // 准备参数 DeptSaveReqVO reqVO = randomPojo(DeptSaveReqVO.class, o -> { @@ -65,12 +100,38 @@ public class DeptServiceImplTest extends BaseDbUnitTest { o.setId(dbDeptDO.getId()); o.setStatus(randomCommonStatus()); }).setDeptSource(1); + reqVO.setCode(dbDeptDO.getCode()); // 调用 deptService.updateDept(reqVO); // 校验是否更新正确 DeptDO deptDO = deptMapper.selectById(reqVO.getId()); // 获取最新的 - assertPojoEquals(reqVO, deptDO); + assertPojoEquals(reqVO, deptDO, "code"); + assertEquals("ZT001", deptDO.getCode()); + } + + @Test + public void testUpdateDept_parentChangedRebuildsCodes() { + Long parentAId = createDept(DeptDO.PARENT_ID_ROOT, "A公司", 1); + Long parentBId = createDept(DeptDO.PARENT_ID_ROOT, "B公司", 2); + Long childId = createDept(parentAId, "子部门", 1); + Long grandChildId = createDept(childId, "子部门-一组", 1); + + DeptDO parentB = deptMapper.selectById(parentBId); + + DeptSaveReqVO updateReq = new DeptSaveReqVO(); + updateReq.setId(childId); + updateReq.setName("子部门"); + updateReq.setParentId(parentBId); + updateReq.setSort(1); + updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + updateReq.setDeptSource(1); + deptService.updateDept(updateReq); + + DeptDO updatedChild = deptMapper.selectById(childId); + DeptDO updatedGrandChild = deptMapper.selectById(grandChildId); + assertEquals(parentB.getCode() + "001", updatedChild.getCode()); + assertEquals(updatedChild.getCode() + "001", updatedGrandChild.getCode()); } @Test @@ -293,4 +354,44 @@ public class DeptServiceImplTest extends BaseDbUnitTest { assertServiceException(() -> deptService.validateDeptList(ids), DEPT_NOT_ENABLE, deptDO.getName()); } + @Test + public void testInitializeDeptCodes() { + DeptDO root1 = randomPojo(DeptDO.class, o -> { + o.setParentId(DeptDO.PARENT_ID_ROOT); + o.setSort(1); + o.setCode(null); + }).setDeptSource(null); + deptMapper.insert(root1); + DeptDO root2 = randomPojo(DeptDO.class, o -> { + o.setParentId(DeptDO.PARENT_ID_ROOT); + o.setSort(2); + o.setCode(null); + }).setDeptSource(null); + deptMapper.insert(root2); + DeptDO child1 = randomPojo(DeptDO.class, o -> { + o.setParentId(root1.getId()); + o.setSort(1); + o.setCode(null); + }).setDeptSource(null); + deptMapper.insert(child1); + DeptDO child2 = randomPojo(DeptDO.class, o -> { + o.setParentId(root1.getId()); + o.setSort(2); + o.setCode(null); + }).setDeptSource(null); + deptMapper.insert(child2); + + deptService.initializeDeptCodes(); + + DeptDO updatedRoot1 = deptMapper.selectById(root1.getId()); + DeptDO updatedRoot2 = deptMapper.selectById(root2.getId()); + DeptDO updatedChild1 = deptMapper.selectById(child1.getId()); + DeptDO updatedChild2 = deptMapper.selectById(child2.getId()); + + assertEquals("ZT001", updatedRoot1.getCode()); + assertEquals("ZT002", updatedRoot2.getCode()); + assertEquals("ZT001001", updatedChild1.getCode()); + assertEquals("ZT001002", updatedChild2.getCode()); + } + } diff --git a/zt-module-template/zt-module-template-server/src/main/resources/application.yaml b/zt-module-template/zt-module-template-server/src/main/resources/application.yaml index a45502b1..7487e84b 100644 --- a/zt-module-template/zt-module-template-server/src/main/resources/application.yaml +++ b/zt-module-template/zt-module-template-server/src/main/resources/application.yaml @@ -124,8 +124,8 @@ zt: - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 swagger: - title: 管理后台 - description: 提供管理员管理的所有功能 + title: 示例模块 + description: 示例模块 version: ${zt.info.version} tenant: # 多租户相关配置项 enable: true