From 29e0c7da14541725e5717acd467adadcf3ef1962 Mon Sep 17 00:00:00 2001 From: chenbowen Date: Tue, 9 Dec 2025 16:24:17 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=96=B0=E5=A2=9E=20api=20=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E5=AE=A2=E6=88=B7=E5=87=AD=E8=AF=81=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/dm/ruoyi-vue-pro-dm8.sql | 2 +- ...总线API凭证绑定与访问日志补充_20251209.sql | 45 +++++++++ zt-dependencies/pom.xml | 18 ---- .../gateway/convert/ApiDefinitionConvert.java | 16 ++++ .../vo/accesslog/ApiAccessLogPageReqVO.java | 6 ++ .../vo/accesslog/ApiAccessLogRespVO.java | 6 ++ .../ApiCredentialBindingRespVO.java | 17 ++++ .../definition/ApiDefinitionDetailRespVO.java | 6 ++ .../vo/definition/ApiDefinitionSaveReqVO.java | 3 + .../dataobject/gateway/ApiAccessLogDO.java | 10 ++ .../gateway/ApiDefinitionCredentialDO.java | 37 ++++++++ .../dal/mysql/gateway/ApiAccessLogMapper.java | 2 + .../ApiDefinitionCredentialMapper.java | 32 +++++++ .../gateway/core/ApiGatewayAccessLogger.java | 26 ++--- .../core/ApiGatewayErrorProcessor.java | 4 + .../core/ApiGatewayExecutionService.java | 82 +++++++--------- .../gateway/core/ApiGatewayRequestMapper.java | 24 +++-- .../gateway/domain/ApiCredentialBinding.java | 18 ++++ .../domain/ApiDefinitionAggregate.java | 6 ++ .../gateway/model/ApiInvocationContext.java | 6 ++ .../CachedBodyHttpServletRequest.java | 21 ++-- .../security/GatewaySecurityFilter.java | 5 +- .../impl/ApiDefinitionServiceImpl.java | 95 +++++++++++++++++++ .../impl/ApiPolicyRateLimitServiceImpl.java | 9 +- .../gateway/impl/ApiVersionServiceImpl.java | 12 +++ .../GatewayServiceErrorCodeConstants.java | 1 + .../src/main/resources/application-dev.yml | 2 + .../src/main/resources/application.yml | 1 + .../sample/DatabusApiInvocationExample.java | 12 ++- .../src/main/resources/application-dev.yaml | 3 +- 30 files changed, 415 insertions(+), 112 deletions(-) create mode 100644 sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiCredentialBindingRespVO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionCredentialDO.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionCredentialMapper.java create mode 100644 zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiCredentialBinding.java diff --git a/sql/dm/ruoyi-vue-pro-dm8.sql b/sql/dm/ruoyi-vue-pro-dm8.sql index 12a37c4f..73b29247 100644 --- a/sql/dm/ruoyi-vue-pro-dm8.sql +++ b/sql/dm/ruoyi-vue-pro-dm8.sql @@ -1625,7 +1625,7 @@ INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon 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 (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:41', '0'); 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 (101, '角色管理', '', 2, 2, 1, 'role', 'ep:user', 'system/role/index', 'SystemRole', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-05-01 18:35:29', '0'); 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 (102, '菜单管理', '', 2, 3, 1, 'menu', 'ep:menu', 'system/menu/index', 'SystemMenu', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:03:50', '0'); -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 (103, '部门管理', '', 2, 4, 1, 'dept', 'fa:address-card', 'system/dept/index', 'SystemDept', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:28', '0'); +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 (103, '组织管理', '', 2, 4, 1, 'dept', 'fa:address-card', 'system/dept/index', 'SystemDept', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:28', '0'); 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 (104, '岗位管理', '', 2, 5, 1, 'post', 'fa:address-book-o', 'system/post/index', 'SystemPost', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:39', '0'); 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 (105, '字典管理', '', 2, 6, 1, 'dict', 'ep:collection', 'system/dict/index', 'SystemDictType', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:07:12', '0'); 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 (106, '配置管理', '', 2, 8, 2, 'config', 'fa:connectdevelop', 'infra/config/index', 'InfraConfig', 0, '1', '1', '1', 'admin', '2021-01-05 17:03:48', '1', '2024-04-23 00:02:45', '0'); diff --git a/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql b/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql new file mode 100644 index 00000000..ab9dcc19 --- /dev/null +++ b/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql @@ -0,0 +1,45 @@ +/* + * Databus API 凭证绑定与访问日志补充字段(DM8) + * Generated on 2025-12-09 + */ + +-- ---------------------------- +-- Table structure for databus_api_definition_credential +-- ---------------------------- +CREATE TABLE databus_api_definition_credential ( + id BIGINT NOT NULL PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + api_id BIGINT NOT NULL, + credential_id BIGINT NOT NULL, + app_id VARCHAR(128), + 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_api_definition_credential ON databus_api_definition_credential (api_id, credential_id, deleted); +CREATE INDEX idx_databus_api_definition_credential_api ON databus_api_definition_credential (api_id); +CREATE INDEX idx_databus_api_definition_credential_cred ON databus_api_definition_credential (credential_id); + +COMMENT ON TABLE databus_api_definition_credential IS 'Databus API 凭证绑定表'; +COMMENT ON COLUMN databus_api_definition_credential.id IS '主键 ID'; +COMMENT ON COLUMN databus_api_definition_credential.tenant_id IS '租户编号'; +COMMENT ON COLUMN databus_api_definition_credential.api_id IS 'API 定义 ID'; +COMMENT ON COLUMN databus_api_definition_credential.credential_id IS '凭证 ID'; +COMMENT ON COLUMN databus_api_definition_credential.app_id IS '凭证应用标识冗余'; +COMMENT ON COLUMN databus_api_definition_credential.creator IS '创建者'; +COMMENT ON COLUMN databus_api_definition_credential.create_time IS '创建时间'; +COMMENT ON COLUMN databus_api_definition_credential.updater IS '更新者'; +COMMENT ON COLUMN databus_api_definition_credential.update_time IS '更新时间'; +COMMENT ON COLUMN databus_api_definition_credential.deleted IS '逻辑删除标记'; + +-- ---------------------------- +-- Alter databus_api_access_log add credential columns +-- ---------------------------- +ALTER TABLE databus_api_access_log ADD credential_app_id VARCHAR(128); +COMMENT ON COLUMN databus_api_access_log.credential_app_id IS '调用凭证应用标识'; + +ALTER TABLE databus_api_access_log ADD credential_id BIGINT; +COMMENT ON COLUMN databus_api_access_log.credential_id IS '调用凭证 ID'; diff --git a/zt-dependencies/pom.xml b/zt-dependencies/pom.xml index cf5b81fe..b0a132d9 100644 --- a/zt-dependencies/pom.xml +++ b/zt-dependencies/pom.xml @@ -32,8 +32,6 @@ 3.4.5 2024.0.1 2023.0.3.2 - - 2.4.0 2.8.3 4.6.0 @@ -89,8 +87,6 @@ 1.2.5 0.9.0 4.12.0 - 11.4.7 - 11.4.7 2.15.1 4.5.13 @@ -136,20 +132,6 @@ import - - - - - org.apache.seata - seata-all - ${seata.version} - - - org.apache.seata - seata-spring-boot-starter - ${seata.version} - - io.github.mouzt diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java index cc2736bd..3bc46e3d 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/convert/ApiDefinitionConvert.java @@ -7,6 +7,7 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.*; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; 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.framework.integration.gateway.domain.ApiCredentialBinding; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiFlowPublication; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; @@ -18,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; @Mapper @@ -48,6 +50,11 @@ public interface ApiDefinitionConvert { detail.setApiLevelTransforms(convertTransforms(aggregate.getDefinition().getId(), aggregate.getApiLevelTransforms().values())); detail.setSteps(convertSteps(aggregate.getSteps())); detail.setPublication(convert(aggregate.getPublication())); + detail.setCredentialBindings(convertCredentialBindings(aggregate.getCredentialBindings())); + detail.setCredentialIds(detail.getCredentialBindings().stream() + .map(ApiCredentialBindingRespVO::getCredentialId) + .filter(Objects::nonNull) + .collect(Collectors.toList())); return detail; } @@ -99,6 +106,15 @@ public interface ApiDefinitionConvert { return publication == null ? null : BeanUtils.toBean(publication, ApiDefinitionPublicationRespVO.class); } + default List convertCredentialBindings(List bindings) { + if (CollUtil.isEmpty(bindings)) { + return new ArrayList<>(); + } + return bindings.stream() + .map(binding -> BeanUtils.toBean(binding, ApiCredentialBindingRespVO.class)) + .collect(Collectors.toList()); + } + /** * 转换步骤列表(DO -> SaveReqVO) */ diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java index 7d012a93..879304a6 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogPageReqVO.java @@ -47,6 +47,12 @@ public class ApiAccessLogPageReqVO extends PageParam { @Schema(description = "请求路径", example = "/gateway/api/user/query") private String requestPath; + @Schema(description = "应用标识", example = "app-portal-01") + private String credentialAppId; + + @Schema(description = "凭证主键", example = "10086") + private Long credentialId; + @Schema(description = "请求时间区间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] requestTime; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java index 00126a9a..a439724c 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/accesslog/ApiAccessLogRespVO.java @@ -33,6 +33,12 @@ public class ApiAccessLogRespVO { @Schema(description = "请求路径", example = "/gateway/api/user/query") private String requestPath; + @Schema(description = "应用标识", example = "app-portal-01") + private String credentialAppId; + + @Schema(description = "凭证主键", example = "10086") + private Long credentialId; + @Schema(description = "查询参数(JSON)") private String requestQuery; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiCredentialBindingRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiCredentialBindingRespVO.java new file mode 100644 index 00000000..1eb4f7ed --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/definition/ApiCredentialBindingRespVO.java @@ -0,0 +1,17 @@ +package com.zt.plat.module.databus.controller.admin.gateway.vo.definition; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class ApiCredentialBindingRespVO { + + @Schema(description = "凭证主键", example = "10086") + private Long credentialId; + + @Schema(description = "应用标识", example = "app-portal-01") + private String appId; + + @Schema(description = "应用名称") + private String appName; +} \ No newline at end of file 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 c64ba1ec..37384ad2 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 @@ -53,6 +53,12 @@ public class ApiDefinitionDetailRespVO { @Schema(description = "API 级别变换列表") private List apiLevelTransforms = new ArrayList<>(); + @Schema(description = "授权凭证 ID 列表") + private List credentialIds = new ArrayList<>(); + + @Schema(description = "授权凭证详情列表") + private List credentialBindings = new ArrayList<>(); + @Schema(description = "步骤列表") private List steps = 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/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 afd54dca..a0375294 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 @@ -46,6 +46,9 @@ public class ApiDefinitionSaveReqVO { @Valid private List apiLevelTransforms = new ArrayList<>(); + @Schema(description = "授权的客户端凭证 ID 列表") + private List credentialIds = new ArrayList<>(); + @Schema(description = "步骤列表") @NotEmpty(message = "编排步骤不能为空") @Valid diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java index f6ab6f90..e46b5507 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiAccessLogDO.java @@ -52,6 +52,16 @@ public class ApiAccessLogDO extends TenantBaseDO { */ private String requestPath; + /** + * 调用使用的应用标识 + */ + private String credentialAppId; + + /** + * 调用使用的凭证主键 + */ + private Long credentialId; + /** * 查询参数(JSON 字符串) */ diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionCredentialDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionCredentialDO.java new file mode 100644 index 00000000..bfd11c89 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiDefinitionCredentialDO.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 与客户端凭证的授权关联。 + */ +@Data +@TableName("databus_api_definition_credential") +@KeySequence("databus_api_definition_credential_seq") +@EqualsAndHashCode(callSuper = true) +public class ApiDefinitionCredentialDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * API 主键 + */ + private Long apiId; + + /** + * 客户端凭证主键 + */ + private Long credentialId; + + /** + * 绑定时的应用标识冗余,便于快速校验 + */ + private String appId; +} \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java index 8d58239b..8fe6d4d9 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiAccessLogMapper.java @@ -20,6 +20,8 @@ public interface ApiAccessLogMapper extends BaseMapperX { .eqIfPresent(ApiAccessLogDO::getResponseStatus, reqVO.getResponseStatus()) .eqIfPresent(ApiAccessLogDO::getStatus, reqVO.getStatus()) .likeIfPresent(ApiAccessLogDO::getClientIp, reqVO.getClientIp()) + .eqIfPresent(ApiAccessLogDO::getCredentialAppId, reqVO.getCredentialAppId()) + .eqIfPresent(ApiAccessLogDO::getCredentialId, reqVO.getCredentialId()) .eqIfPresent(ApiAccessLogDO::getTenantId, reqVO.getTenantId()) .likeIfPresent(ApiAccessLogDO::getRequestPath, reqVO.getRequestPath()); if (ArrayUtil.isNotEmpty(reqVO.getRequestTime()) && reqVO.getRequestTime().length == 2) { diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionCredentialMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionCredentialMapper.java new file mode 100644 index 00000000..6ed1552d --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/mysql/gateway/ApiDefinitionCredentialMapper.java @@ -0,0 +1,32 @@ +package com.zt.plat.module.databus.dal.mysql.gateway; + +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionCredentialDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ApiDefinitionCredentialMapper extends BaseMapperX { + + default List selectByApiId(Long apiId) { + return selectList(new LambdaQueryWrapperX() + .eq(ApiDefinitionCredentialDO::getApiId, apiId)); + } + + default void deleteByApiId(Long apiId) { + delete(new LambdaQueryWrapperX() + .eq(ApiDefinitionCredentialDO::getApiId, apiId)); + } + + /** + * 按 API 逻辑删除已有绑定,保留操作记录。 + */ + default void logicDeleteByApiId(Long apiId) { + ApiDefinitionCredentialDO entity = new ApiDefinitionCredentialDO(); + entity.setDeleted(Boolean.TRUE); + update(entity, new LambdaQueryWrapperX() + .eq(ApiDefinitionCredentialDO::getApiId, apiId)); + } +} \ 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/ApiGatewayAccessLogger.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java index bce32b79..b0ed9987 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java @@ -83,7 +83,7 @@ public class ApiGatewayAccessLogger { try { ApiAccessLogDO update = new ApiAccessLogDO(); update.setId(logId); - int responseStatus = resolveHttpStatus(context); + Integer responseStatus = resolveHttpStatus(context); context.setResponseStatus(responseStatus); update.setResponseStatus(responseStatus); String responseMessage = resolveResponseMessage(context, responseStatus); @@ -193,6 +193,8 @@ public class ApiGatewayAccessLogger { logDO.setRequestBody(toJson(context.getRequestBody())); logDO.setClientIp(context.getClientIp()); logDO.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT)); + logDO.setCredentialAppId(context.getCredentialAppId()); + logDO.setCredentialId(context.getCredentialId()); logDO.setStatus(3); logDO.setRequestTime(toLocalDateTime(context.getRequestTime())); logDO.setTenantId(parseTenantId(context.getTenantId())); @@ -231,7 +233,7 @@ public class ApiGatewayAccessLogger { return 3; } - private String resolveErrorMessage(ApiInvocationContext context, int responseStatus) { + private String resolveErrorMessage(ApiInvocationContext context, Integer responseStatus) { if (!isErrorStatus(responseStatus)) { return null; } @@ -248,7 +250,7 @@ public class ApiGatewayAccessLogger { return null; } - private String extractErrorCode(Object responseBody, int responseStatus) { + private String extractErrorCode(Object responseBody, Integer responseStatus) { if (!isErrorStatus(responseStatus)) { return null; } @@ -259,16 +261,14 @@ public class ApiGatewayAccessLogger { return null; } - private int resolveHttpStatus(ApiInvocationContext context) { - Integer status = context.getResponseStatus(); - if (status != null) { - return status; - } - // 默认兜底为 200,避免日志中出现空的 HTTP 状态码 - return HttpStatus.OK.value(); + private Integer resolveHttpStatus(ApiInvocationContext context) { + return context.getResponseStatus(); } - private String resolveResponseMessage(ApiInvocationContext context, int responseStatus) { + private String resolveResponseMessage(ApiInvocationContext context, Integer responseStatus) { + if (responseStatus == null) { + return null; + } if (StringUtils.hasText(context.getResponseMessage())) { return truncate(context.getResponseMessage()); } @@ -276,8 +276,8 @@ public class ApiGatewayAccessLogger { return resolved != null ? resolved.getReasonPhrase() : null; } - private boolean isErrorStatus(int responseStatus) { - return responseStatus >= 400; + private boolean isErrorStatus(Integer responseStatus) { + return responseStatus != null && responseStatus >= 400; } private Map buildExtra(ApiInvocationContext context) { 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 index e7b9dbcb..f24dfc08 100644 --- 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 @@ -10,6 +10,7 @@ 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_CREDENTIAL_UNAUTHORIZED; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EXCEEDED; /** @@ -80,6 +81,9 @@ public class ApiGatewayErrorProcessor { if (API_RATE_LIMIT_EXCEEDED.getCode().equals(code)) { return HttpStatus.TOO_MANY_REQUESTS.value(); } + if (API_CREDENTIAL_UNAUTHORIZED.getCode().equals(code)) { + return HttpStatus.FORBIDDEN.value(); + } } return HttpStatus.INTERNAL_SERVER_ERROR.value(); } 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 index 8f442153..f1f39fbb 100644 --- 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 @@ -7,10 +7,10 @@ import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; import com.zt.plat.framework.common.util.monitor.TracerUtils; 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.domain.ApiCredentialBinding; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; 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 com.zt.plat.module.databus.service.gateway.ApiDefinitionService; import lombok.RequiredArgsConstructor; @@ -19,6 +19,7 @@ import org.springframework.http.*; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.util.UriComponentsBuilder; @@ -29,12 +30,12 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_UNAUTHORIZED; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_DEFINITION_NOT_FOUND; /** - * 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}). + * 统一处理 API 门户的请求映射、分发与响应构建。 + * 管理端调试与外部 HTTP 请求共享同一套逻辑,安全校验由 {@link GatewaySecurityFilter} 执行。 */ @Slf4j @Component @@ -58,7 +59,7 @@ public class ApiGatewayExecutionService { private final ApiDefinitionService apiDefinitionService; /** - * Maps a raw HTTP message (as provided by Spring Integration) into a context message. + * 将 Spring Integration 提供的原始消息映射为网关上下文消息。 */ public Message mapRequest(Message message) { ApiInvocationContext context = requestMapper.map(message.getPayload(), message.getHeaders()); @@ -70,7 +71,7 @@ public class ApiGatewayExecutionService { } /** - * Dispatches the API invocation and applies gateway error processing rules on failure scenarios. + * 分发 API 调用,并在异常场景套用统一错误处理。 */ public ApiInvocationContext dispatch(Message message) { ApiInvocationContext context = message.getPayload(); @@ -78,6 +79,7 @@ public class ApiGatewayExecutionService { ApiInvocationContext responseContext; ApiDefinitionAggregate debugAggregate = null; try { + enforceCredentialAuthorization(context); if (Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) { debugAggregate = resolveDebugAggregate(context); } @@ -128,7 +130,7 @@ public class ApiGatewayExecutionService { 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); context.getAttributes().put(ATTR_DEBUG_INVOKE, Boolean.TRUE); ApiInvocationContext responseContext = dispatch(mappedMessage); @@ -155,7 +157,7 @@ public class ApiGatewayExecutionService { 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 basePath = properties.getBasePath(); String rawQuery = buildQueryString(reqVO.getQueryParams()); String requestUri = basePath + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion(); if (StringUtils.hasText(rawQuery)) { @@ -169,7 +171,6 @@ public class ApiGatewayExecutionService { if (reqVO.getHeaders() != null) { requestHeaders.putAll(reqVO.getHeaders()); } - normalizeJwtHeaders(requestHeaders, reqVO.getQueryParams()); requestHeaders.putIfAbsent(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); builder.setHeader(HEADER_REQUEST_HEADERS, requestHeaders); requestHeaders.forEach((key, value) -> { @@ -223,8 +224,7 @@ public class ApiGatewayExecutionService { context.setHttpMethod(HttpMethod.POST.name()); } if (!StringUtils.hasText(context.getRequestPath())) { - String basePath = normalizeBasePath(properties.getBasePath()); - String path = basePath + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion(); + String path = properties.getBasePath() + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion(); context.setRequestPath(path); } } @@ -245,15 +245,29 @@ public class ApiGatewayExecutionService { .build(); } - private String normalizeBasePath(String basePath) { - if (!StringUtils.hasText(basePath)) { - return ApiGatewayProperties.DEFAULT_BASE_PATH; + /** + * 调用前校验凭证白名单,非调试调用需匹配绑定的 appId。 + */ + private void enforceCredentialAuthorization(ApiInvocationContext context) { + if (Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) { + return; } - String normalized = basePath.startsWith("/") ? basePath : "/" + basePath; - while (normalized.endsWith("/") && normalized.length() > 1) { - normalized = normalized.substring(0, normalized.length() - 1); + ApiDefinitionAggregate aggregate = apiDefinitionService.findByCodeAndVersion(context.getApiCode(), context.getApiVersion()) + .orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND)); + if (CollectionUtils.isEmpty(aggregate.getCredentialBindings())) { + return; + } + String appId = context.getCredentialAppId(); + if (!StringUtils.hasText(appId)) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_UNAUTHORIZED); + } + boolean matched = aggregate.getCredentialBindings().stream() + .map(ApiCredentialBinding::getAppId) + .filter(StringUtils::hasText) + .anyMatch(boundAppId -> appId.trim().equalsIgnoreCase(boundAppId)); + if (!matched) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_UNAUTHORIZED); } - return normalized; } private String buildQueryString(Map queryParams) { @@ -290,36 +304,4 @@ public class ApiGatewayExecutionService { 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 5d813d52..1c4ba0da 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 @@ -35,6 +35,7 @@ public class ApiGatewayRequestMapper { 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_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress"; + private static final String HEADER_CREDENTIAL_ID = "X-Databus-Credential-Id"; @SuppressWarnings("unchecked") public ApiInvocationContext map(Object payload, Map headers) { @@ -79,18 +80,29 @@ public class ApiGatewayRequestMapper { } Map requestHeaders = (Map) headers.get(HEADER_REQUEST_HEADERS); - GatewayHeaderUtils.mergeNormalizedHeaders(requestHeaders, context.getRequestHeaders()); + if (requestHeaders != null) { + context.getRequestHeaders().putAll(requestHeaders); + } headers.forEach((key, value) -> { - if (isInternalHeader(key)) { + if (isInternalHeader(key) || value == null) { return; } - String normalized = GatewayHeaderUtils.normalizeHeaderValue(value); - if (normalized != null) { - context.getRequestHeaders().putIfAbsent(key, normalized); - } + context.getRequestHeaders().putIfAbsent(key, String.valueOf(value)); }); context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT)); context.setClientIp(resolveClientIp(headers, context.getRequestHeaders())); + String appId = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), ApiGatewayProperties.APP_ID_HEADER); + if (StringUtils.hasText(appId)) { + context.setCredentialAppId(appId.trim()); + } + String credentialIdHeader = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HEADER_CREDENTIAL_ID); + if (StringUtils.hasText(credentialIdHeader)) { + try { + context.setCredentialId(Long.valueOf(credentialIdHeader.trim())); + } catch (NumberFormatException ignored) { + context.setCredentialId(null); + } + } captureAccessLogId(context); populateQueryParams(headers, context, originalRequestUri); if (properties.isEnableTenantHeader()) { diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiCredentialBinding.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiCredentialBinding.java new file mode 100644 index 00000000..6fd70b47 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiCredentialBinding.java @@ -0,0 +1,18 @@ +package com.zt.plat.module.databus.framework.integration.gateway.domain; + +import lombok.Builder; +import lombok.Value; + +/** + * API 授权绑定的凭证信息。 + */ +@Value +@Builder +public class ApiCredentialBinding { + + Long credentialId; + + String appId; + + String appName; +} \ 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/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 24121989..730bc36a 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 @@ -26,6 +26,8 @@ public class ApiDefinitionAggregate { ApiFlowPublication publication; + List credentialBindings; + public List getSteps() { return steps == null ? Collections.emptyList() : steps; } @@ -34,4 +36,8 @@ public class ApiDefinitionAggregate { return apiLevelTransforms == null ? Collections.emptyMap() : apiLevelTransforms; } + public List getCredentialBindings() { + return credentialBindings == null ? Collections.emptyList() : credentialBindings; + } + } 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 a17dc89f..f27b4fbf 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 @@ -29,6 +29,10 @@ public class ApiInvocationContext { private String userAgent; + private String credentialAppId; + + private Long credentialId; + private String httpMethod; private String requestPath; @@ -72,6 +76,8 @@ public class ApiInvocationContext { copy.tenantId = this.tenantId; copy.clientIp = this.clientIp; copy.userAgent = this.userAgent; + copy.credentialAppId = this.credentialAppId; + copy.credentialId = this.credentialId; copy.httpMethod = this.httpMethod; copy.requestPath = this.requestPath; copy.requestBody = this.requestBody; 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 index f722c8f1..0048bf19 100644 --- 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 @@ -15,11 +15,11 @@ 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 java.util.TreeSet; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -104,7 +104,7 @@ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { return; } if (!StringUtils.hasText(value)) { - additionalHeaders.remove(name); + removeHeader(name); return; } additionalHeaders.put(name, new ArrayList<>(Collections.singletonList(value))); @@ -115,7 +115,7 @@ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { return; } if (CollectionUtils.isEmpty(values)) { - additionalHeaders.remove(name); + removeHeader(name); return; } additionalHeaders.put(name, new ArrayList<>(values)); @@ -125,7 +125,7 @@ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { if (!StringUtils.hasText(name) || !StringUtils.hasText(value)) { return; } - additionalHeaders.compute(name, (key, existing) -> { + additionalHeaders.compute(name, (k, existing) -> { List list = existing == null ? new ArrayList<>() : new ArrayList<>(existing); list.add(value); return list; @@ -152,23 +152,20 @@ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { @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); + return Collections.enumeration(custom); } } - Enumeration parent = super.getHeaders(name); - while (parent.hasMoreElements()) { - combined.add(parent.nextElement()); - } - return Collections.enumeration(combined); + return super.getHeaders(name); } @Override public Enumeration getHeaderNames() { - Set names = new LinkedHashSet<>(); + // 使用忽略大小写的集合,避免父请求头名与自定义头名大小写不同而重复 + Set names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); Enumeration parent = super.getHeaderNames(); while (parent.hasMoreElements()) { names.add(parent.nextElement()); 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 6698602b..48c2a627 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 @@ -61,6 +61,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { private final AntPathMatcher pathMatcher = new AntPathMatcher(); private static final TypeReference> MAP_TYPE = new TypeReference<>() { }; + public static final String HEADER_CREDENTIAL_ID = "X-Databus-Credential-Id"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -133,6 +134,8 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { // 使用可重复读取的请求包装,供后续过滤器继续消费 CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody); + securedRequest.setHeader(APP_ID_HEADER, credential.getAppId()); + securedRequest.setHeader(HEADER_CREDENTIAL_ID, credential.getId() != null ? String.valueOf(credential.getId()) : null); ApiGatewayAccessLogger.propagateLogIdHeader(securedRequest, accessLogId); if (StringUtils.hasText(request.getCharacterEncoding())) { securedRequest.setCharacterEncoding(request.getCharacterEncoding()); @@ -283,7 +286,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { try { boolean valid = CryptoSignatureUtils.verifySignature(signaturePayload, signatureType); if (!valid) { -// throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "签名校验失败"); + throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "签名校验失败"); } } catch (IllegalArgumentException ex) { throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "签名算法配置异常"); 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 4c147b19..77511c9a 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 @@ -19,6 +19,7 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefi import com.zt.plat.module.databus.dal.dataobject.gateway.*; import com.zt.plat.module.databus.dal.mysql.gateway.*; import com.zt.plat.module.databus.enums.gateway.ApiStatusEnum; +import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiCredentialBinding; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiFlowPublication; import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition; @@ -40,6 +41,7 @@ import org.springframework.util.StringUtils; import java.time.Duration; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.*; @@ -55,6 +57,8 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { private final ApiTransformMapper apiTransformMapper; private final ApiPolicyRateLimitMapper apiPolicyRateLimitMapper; private final ApiFlowPublishMapper apiFlowPublishMapper; + private final ApiDefinitionCredentialMapper apiDefinitionCredentialMapper; + private final ApiClientCredentialMapper apiClientCredentialMapper; private final ObjectMapper objectMapper; private final StringRedisTemplate stringRedisTemplate; private final ObjectProvider apiVersionServiceProvider; @@ -129,6 +133,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { validateDuplication(reqVO, null); validateStructure(reqVO); validatePolicies(reqVO); + validateCredentials(reqVO.getCredentialIds()); ApiDefinitionDO definition = buildDefinitionDO(reqVO, null); apiDefinitionMapper.insert(definition); @@ -136,6 +141,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { persistApiLevelTransforms(apiId, reqVO.getApiLevelTransforms()); persistSteps(apiId, reqVO.getSteps()); + persistCredentialBindings(apiId, reqVO.getCredentialIds()); String operator = SecurityFrameworkUtils.getLoginUserNickname(); String description = String.format("创建 API (%s)", reqVO.getVersion()); @@ -154,6 +160,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { validateDuplication(reqVO, existing.getId()); validateStructure(reqVO); validatePolicies(reqVO); + validateCredentials(reqVO.getCredentialIds()); ApiDefinitionDO updateObj = buildDefinitionDO(reqVO, existing); apiDefinitionMapper.updateById(updateObj); @@ -161,8 +168,10 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { invalidateCache(existing.getTenantId(), existing.getApiCode(), existing.getVersion()); apiTransformMapper.deleteByApiId(existing.getId()); apiStepMapper.deleteByApiId(existing.getId()); + apiDefinitionCredentialMapper.deleteByApiId(existing.getId()); persistApiLevelTransforms(existing.getId(), reqVO.getApiLevelTransforms()); persistSteps(existing.getId(), reqVO.getSteps()); + persistCredentialBindings(existing.getId(), reqVO.getCredentialIds()); invalidateCache(updateObj.getTenantId(), updateObj.getApiCode(), updateObj.getVersion()); } finally { if (skipSnapshot) { @@ -186,6 +195,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { invalidateCache(existing.getTenantId(), existing.getApiCode(), existing.getVersion()); apiTransformMapper.deleteByApiId(id); apiStepMapper.deleteByApiId(id); + apiDefinitionCredentialMapper.deleteByApiId(id); apiDefinitionMapper.deleteById(id); } @@ -283,12 +293,14 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { ApiFlowPublication publication = apiFlowPublishMapper.selectActiveByApiId(definition.getId()) .map(this::convertPublication) .orElse(null); + List credentialBindings = loadCredentialBindings(definition.getId()); return ApiDefinitionAggregate.builder() .definition(definition) .steps(stepDefinitions) .apiLevelTransforms(apiTransforms) .rateLimitPolicy(rateLimitPolicy) .publication(publication) + .credentialBindings(credentialBindings) .build(); } @@ -497,6 +509,19 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { } } + private void validateCredentials(List credentialIds) { + if (CollectionUtils.isEmpty(credentialIds)) { + return; + } + List credentials = apiClientCredentialMapper.selectBatchIds(credentialIds); + long validCount = credentials == null ? 0 : credentials.stream() + .filter(credential -> credential != null && !Boolean.TRUE.equals(credential.getDeleted())) + .count(); + if (validCount != credentialIds.size()) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_NOT_FOUND); + } + } + private Long resolveTenantIdentifier() { return TenantContextHolder.getTenantId(); } @@ -515,4 +540,74 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { return tenantPart + ":" + apiCode.toLowerCase(Locale.ROOT) + ":" + version; } + /** + * 先删除旧绑定,再对去重后的 credentialIds 批量插入,避免唯一约束冲突。 + */ + private void persistCredentialBindings(Long apiId, List credentialIds) { + // 先逻辑删除当前 API 的旧绑定,保留历史,同时避免重复插入 + apiDefinitionCredentialMapper.logicDeleteByApiId(apiId); + + if (CollectionUtils.isEmpty(credentialIds)) { + return; + } + + // 去重后再查询有效凭证 + List distinctIds = credentialIds.stream() + .filter(Objects::nonNull) + .distinct() + .toList(); + if (CollectionUtils.isEmpty(distinctIds)) { + return; + } + + List credentials = apiClientCredentialMapper.selectBatchIds(distinctIds); + if (CollectionUtils.isEmpty(credentials)) { + return; + } + + for (ApiClientCredentialDO credential : credentials) { + if (credential == null || Boolean.TRUE.equals(credential.getDeleted())) { + continue; + } + ApiDefinitionCredentialDO relation = new ApiDefinitionCredentialDO(); + relation.setId(null); + relation.setApiId(apiId); + relation.setCredentialId(credential.getId()); + relation.setAppId(credential.getAppId()); + relation.setDeleted(Boolean.FALSE); + apiDefinitionCredentialMapper.insert(relation); + } + } + + private List loadCredentialBindings(Long apiId) { + List relations = apiDefinitionCredentialMapper.selectByApiId(apiId); + if (CollectionUtils.isEmpty(relations)) { + return Collections.emptyList(); + } + List credentialIds = relations.stream() + .map(ApiDefinitionCredentialDO::getCredentialId) + .filter(Objects::nonNull) + .toList(); + Map credentialMap = Collections.emptyMap(); + if (!CollectionUtils.isEmpty(credentialIds)) { + List credentials = apiClientCredentialMapper.selectBatchIds(credentialIds); + if (!CollectionUtils.isEmpty(credentials)) { + credentialMap = credentials.stream() + .filter(credential -> credential != null && !Boolean.TRUE.equals(credential.getDeleted())) + .collect(Collectors.toMap(ApiClientCredentialDO::getId, c -> c, (a, b) -> a)); + } + } + List bindings = new ArrayList<>(relations.size()); + for (ApiDefinitionCredentialDO relation : relations) { + ApiClientCredentialDO credential = relation.getCredentialId() == null ? null : credentialMap.get(relation.getCredentialId()); + ApiCredentialBinding binding = ApiCredentialBinding.builder() + .credentialId(relation.getCredentialId()) + .appId(relation.getAppId()) + .appName(credential != null ? credential.getAppName() : null) + .build(); + bindings.add(binding); + } + return bindings; + } + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyRateLimitServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyRateLimitServiceImpl.java index 64b71025..6a2214b4 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyRateLimitServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiPolicyRateLimitServiceImpl.java @@ -86,13 +86,8 @@ public class ApiPolicyRateLimitServiceImpl implements ApiPolicyRateLimitService private void apply(ApiPolicySaveReqVO reqVO, ApiPolicyRateLimitDO 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; + target.setConfig(StrUtil.trim(reqVO.getConfig())); + target.setDescription(StrUtil.trim(reqVO.getDescription())); } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java index f4961fa1..91d3a5ef 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiVersionServiceImpl.java @@ -11,11 +11,13 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefi import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionCompareRespVO; import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO; import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionCredentialDO; 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.dataobject.gateway.ApiVersionDO; import com.zt.plat.module.databus.dal.mapper.gateway.ApiVersionMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper; +import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionCredentialMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper; import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; @@ -47,6 +49,7 @@ public class ApiVersionServiceImpl implements ApiVersionService { private final ApiDefinitionMapper apiDefinitionMapper; private final ApiStepMapper apiStepMapper; private final ApiTransformMapper apiTransformMapper; + private final ApiDefinitionCredentialMapper apiDefinitionCredentialMapper; private final ObjectMapper objectMapper; private final ApiDefinitionService apiDefinitionService; @@ -191,6 +194,15 @@ public class ApiVersionServiceImpl implements ApiVersionService { snapshot.setApiLevelTransforms(ApiDefinitionConvert.INSTANCE.convertTransformList(apiTransforms)); } + List credentialRelations = apiDefinitionCredentialMapper.selectByApiId(apiId); + if (credentialRelations != null && !credentialRelations.isEmpty()) { + List credentialIds = credentialRelations.stream() + .map(ApiDefinitionCredentialDO::getCredentialId) + .filter(Objects::nonNull) + .toList(); + snapshot.setCredentialIds(credentialIds); + } + return snapshot; } 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 08ee01c5..334b891c 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 @@ -61,5 +61,6 @@ public interface GatewayServiceErrorCodeConstants { ErrorCode API_VERSION_SNAPSHOT_DESERIALIZE_FAILED = new ErrorCode(1_010_000_052, "API 版本快照反序列化失败"); ErrorCode API_VERSION_ACTIVE_CANNOT_DELETE = new ErrorCode(1_010_000_053, "当前激活版本不允许删除"); ErrorCode API_VERSION_API_MISMATCH = new ErrorCode(1_010_000_054, "两个版本不属于同一 API"); + ErrorCode API_CREDENTIAL_UNAUTHORIZED = new ErrorCode(1_010_000_055, "当前凭证无权访问该 API"); } diff --git a/zt-module-databus/zt-module-databus-server/src/main/resources/application-dev.yml b/zt-module-databus/zt-module-databus-server/src/main/resources/application-dev.yml index 892d171e..87dad43b 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/resources/application-dev.yml +++ b/zt-module-databus/zt-module-databus-server/src/main/resources/application-dev.yml @@ -52,6 +52,8 @@ spring: host: 172.16.46.63 # 地址 port: 30379 # 端口 database: 0 # 数据库索引 + username: zt-redis + password: P@ssword25 # password: 123456 # 密码,建议生产环境开启 xxl: 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 5a113239..93b7aa62 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 @@ -130,6 +130,7 @@ zt: - /databus/api/portal/** ignore-tables: - databus_api_client_credential + - databus_api_definition_credential # DataBus 数据同步服务端配置 databus: sync: diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java index d964863a..03a2040a 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java @@ -28,10 +28,16 @@ import java.util.UUID; public final class DatabusApiInvocationExample { public static final String TIMESTAMP = Long.toString(System.currentTimeMillis()); - private static final String APP_ID = "ztmy"; - private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; + + private static final String APP_ID = "iwork"; + private static final String APP_SECRET = "lpGXiNe/GMLk0vsbYGLa8eYxXq8tGhTbuu3/D4MJzIk="; + // private static final String APP_ID = "ztmy"; + // private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES; - private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1"; +// private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1"; +// private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/callback/v1"; +// private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/callback/v1"; + private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/testcbw/456"; private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); diff --git a/zt-module-infra/zt-module-infra-server/src/main/resources/application-dev.yaml b/zt-module-infra/zt-module-infra-server/src/main/resources/application-dev.yaml index 5f4da0a6..9a4b7a79 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/resources/application-dev.yaml +++ b/zt-module-infra/zt-module-infra-server/src/main/resources/application-dev.yaml @@ -53,7 +53,8 @@ spring: host: 172.16.46.63 # 地址 port: 30379 # 端口 database: 1 # 数据库索引 -# password: 123456 # 密码,建议生产环境开启 + username: zt-redis + password: P@ssword25 --- #################### MQ 消息队列相关配置 ####################