1. 新增外部系统编码部门编码关联管理
2. 新增统一的 api 对外门户管理 3. 修正各个模块的 api 命名
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -22,7 +22,7 @@
|
||||
<!-- <module>zt-module-ai</module>-->
|
||||
<module>zt-module-template</module>
|
||||
<!-- <module>zt-module-iot</module>-->
|
||||
<!-- <module>zt-module-databus</module>-->
|
||||
<module>zt-module-databus</module>
|
||||
<!-- <module>zt-module-rule</module>-->
|
||||
<!-- <module>zt-module-html2pdf</module>-->
|
||||
</modules>
|
||||
|
||||
@@ -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
|
||||
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');
|
||||
|
||||
@@ -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 '逻辑删除标记';
|
||||
|
||||
@@ -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<String, Object> reqMap, String type) {
|
||||
Map<String, Object> sortedMap = new TreeMap<>(reqMap);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, Object> 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<String> buildBase64Candidates(String ciphertext) {
|
||||
Set<String> 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String> includePaths = properties.getIncludePaths();
|
||||
if (CollectionUtils.isEmpty(includePaths)) {
|
||||
registration.addPathPatterns("/**");
|
||||
} else {
|
||||
registration.addPathPatterns(includePaths.toArray(new String[0]));
|
||||
}
|
||||
List<String> excludePaths = properties.getExcludePaths();
|
||||
if (!CollectionUtils.isEmpty(excludePaths)) {
|
||||
registration.excludePathPatterns(excludePaths.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, String> parameterMap = getRequestParameterMap(request);
|
||||
SortedMap<String, String> headerMap = getRequestHeaderMap(rule, request);
|
||||
String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), "");
|
||||
return MapUtil.join(parameterMap, "&", "=")
|
||||
+ requestBody
|
||||
+ MapUtil.join(headerMap, "&", "=")
|
||||
+ appSecret;
|
||||
}
|
||||
|
||||
private SortedMap<String, String> getRequestHeaderMap(ApiSignatureRule rule, HttpServletRequest request) {
|
||||
SortedMap<String, String> 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<String, String> getRequestParameterMap(HttpServletRequest request) {
|
||||
SortedMap<String, String> sortedMap = new TreeMap<>();
|
||||
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
|
||||
sortedMap.put(entry.getKey(), entry.getValue()[0]);
|
||||
}
|
||||
return sortedMap;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验请求头加签参数
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建签名字符串
|
||||
* <p>
|
||||
* 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
|
||||
*
|
||||
* @param signature signature
|
||||
* @param request request
|
||||
* @param appSecret appSecret
|
||||
* @return 签名字符串
|
||||
*/
|
||||
private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) {
|
||||
SortedMap<String, String> parameterMap = getRequestParameterMap(request); // 请求头
|
||||
SortedMap<String, String> 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<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) {
|
||||
SortedMap<String, String> 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<String, String> getRequestParameterMap(HttpServletRequest request) {
|
||||
SortedMap<String, String> sortedMap = new TreeMap<>();
|
||||
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
|
||||
sortedMap.put(entry.getKey(), entry.getValue()[0]);
|
||||
}
|
||||
return sortedMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> includePaths = new ArrayList<>(Arrays.asList("/**"));
|
||||
|
||||
/**
|
||||
* 无需签名校验的路径
|
||||
*/
|
||||
private List<String> excludePaths = new ArrayList<>(Arrays.asList("/error", "/swagger-ui/**", "/v3/api-docs/**"));
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> includePaths = properties.getIncludePaths();
|
||||
if (!CollectionUtils.isEmpty(includePaths)) {
|
||||
boolean matched = includePaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
|
||||
if (!matched) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
|
||||
private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE
|
||||
= new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {};
|
||||
|
||||
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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -140,11 +140,6 @@
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-scripting</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Reactive HTTP client for internal REST orchestration -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -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<PageResult<ApiClientCredentialRespVO>> page(ApiClientCredentialPageReqVO reqVO) {
|
||||
PageResult<ApiClientCredentialDO> page = credentialService.getPage(reqVO);
|
||||
return success(ApiClientCredentialConvert.INSTANCE.convertPage(page));
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "查询凭证详情")
|
||||
public CommonResult<ApiClientCredentialRespVO> get(@RequestParam("id") Long id) {
|
||||
ApiClientCredentialDO credential = credentialService.get(id);
|
||||
return success(ApiClientCredentialConvert.INSTANCE.convert(credential));
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "新增客户端凭证")
|
||||
public CommonResult<Long> create(@Valid @RequestBody ApiClientCredentialSaveReqVO reqVO) {
|
||||
return success(credentialService.create(reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新客户端凭证")
|
||||
public CommonResult<Boolean> update(@Valid @RequestBody ApiClientCredentialSaveReqVO reqVO) {
|
||||
credentialService.update(reqVO);
|
||||
return success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除客户端凭证")
|
||||
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
|
||||
credentialService.delete(id);
|
||||
return success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@GetMapping("/list-simple")
|
||||
@Operation(summary = "获取启用的凭证列表(精简)")
|
||||
public CommonResult<List<ApiClientCredentialSimpleRespVO>> listSimple() {
|
||||
List<ApiClientCredentialDO> list = credentialService.listEnabled();
|
||||
return success(ApiClientCredentialConvert.INSTANCE.convertSimpleList(list));
|
||||
}
|
||||
}
|
||||
@@ -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<ApiGatewayResponse> 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<ApiGatewayResponse> 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<String, Object> 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<String, Object> body = new HashMap<>();
|
||||
body.put("errorMessage", message);
|
||||
body.put("exception", ex.getClass().getSimpleName());
|
||||
context.setResponseBody(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PageResult<ApiPolicyRespVO>> getAuthPolicyPage(@Valid ApiPolicyPageReqVO reqVO) {
|
||||
PageResult<ApiPolicyAuthDO> pageResult = authService.getPage(reqVO);
|
||||
return success(ApiPolicyAuthConvert.INSTANCE.convertPage(pageResult));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "查询认证策略详情")
|
||||
public CommonResult<ApiPolicyRespVO> 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<List<ApiPolicySimpleRespVO>> getAuthPolicySimpleList() {
|
||||
List<ApiPolicyAuthDO> list = authService.getSimpleList();
|
||||
return success(ApiPolicyAuthConvert.INSTANCE.convertSimpleList(list));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建认证策略")
|
||||
public CommonResult<Long> createAuthPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
|
||||
Long id = authService.create(reqVO);
|
||||
return success(id);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "更新认证策略")
|
||||
public CommonResult<Boolean> updateAuthPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
|
||||
authService.update(reqVO);
|
||||
return success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除认证策略")
|
||||
public CommonResult<Boolean> deleteAuthPolicy(@PathVariable("id") Long id) {
|
||||
authService.delete(id);
|
||||
return success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ApiClientCredentialRespVO> convertList(List<ApiClientCredentialDO> list);
|
||||
|
||||
default PageResult<ApiClientCredentialRespVO> convertPage(PageResult<ApiClientCredentialDO> page) {
|
||||
if (page == null) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
PageResult<ApiClientCredentialRespVO> result = new PageResult<>();
|
||||
result.setList(convertList(page.getList()));
|
||||
result.setTotal(page.getTotal());
|
||||
return result;
|
||||
}
|
||||
|
||||
default List<ApiClientCredentialSimpleRespVO> convertSimpleList(List<ApiClientCredentialDO> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<ApiPolicyRespVO> convertList(List<ApiPolicyAuthDO> list);
|
||||
|
||||
PageResult<ApiPolicyRespVO> convertPage(PageResult<ApiPolicyAuthDO> page);
|
||||
|
||||
List<ApiPolicySimpleRespVO> convertSimpleList(List<ApiPolicyAuthDO> list);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ApiDefinitionTransformSaveReqVO> apiLevelTransforms = new ArrayList<>();
|
||||
|
||||
@@ -37,9 +37,6 @@ public class ApiDefinitionStepRespVO {
|
||||
@Schema(description = "超时时间(毫秒)")
|
||||
private Long timeout;
|
||||
|
||||
@Schema(description = "重试策略(JSON)")
|
||||
private String retryStrategy;
|
||||
|
||||
@Schema(description = "降级策略(JSON)")
|
||||
private String fallbackStrategy;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -38,8 +38,6 @@ public class ApiStepDO extends TenantBaseDO {
|
||||
|
||||
private Long timeout;
|
||||
|
||||
private String retryStrategy;
|
||||
|
||||
private String fallbackStrategy;
|
||||
|
||||
private String conditionExpr;
|
||||
|
||||
@@ -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<ApiClientCredentialDO> {
|
||||
|
||||
default Optional<ApiClientCredentialDO> selectByAppId(String appId) {
|
||||
if (StrUtil.isBlank(appId)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
LambdaQueryWrapperX<ApiClientCredentialDO> query = new LambdaQueryWrapperX<>();
|
||||
query.eq(ApiClientCredentialDO::getAppId, appId)
|
||||
.eq(ApiClientCredentialDO::getDeleted, false);
|
||||
return Optional.ofNullable(selectOne(query));
|
||||
}
|
||||
|
||||
default PageResult<ApiClientCredentialDO> selectPage(ApiClientCredentialPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ApiClientCredentialDO> 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<ApiClientCredentialDO> selectEnabledList() {
|
||||
return selectList(new LambdaQueryWrapperX<ApiClientCredentialDO>()
|
||||
.eq(ApiClientCredentialDO::getEnabled, true)
|
||||
.eq(ApiClientCredentialDO::getDeleted, false)
|
||||
.orderByAsc(ApiClientCredentialDO::getAppId));
|
||||
}
|
||||
}
|
||||
@@ -31,26 +31,15 @@ public interface ApiDefinitionMapper extends BaseMapperX<ApiDefinitionDO> {
|
||||
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<ApiDefinitionDO>()
|
||||
.eq(ApiDefinitionDO::getAuthPolicyId, policyId)
|
||||
.eq(ApiDefinitionDO::getDeleted, false));
|
||||
}
|
||||
|
||||
default Long selectCountByRateLimitPolicyId(Long policyId) {
|
||||
if (policyId == null) {
|
||||
return 0L;
|
||||
|
||||
@@ -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<ApiPolicyAuthDO> {
|
||||
|
||||
default PageResult<ApiPolicyAuthDO> selectPage(ApiPolicyPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ApiPolicyAuthDO> 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<ApiPolicyAuthDO> selectSimpleList() {
|
||||
return selectList(new LambdaQueryWrapperX<ApiPolicyAuthDO>()
|
||||
.eq(ApiPolicyAuthDO::getDeleted, false)
|
||||
.orderByDesc(ApiPolicyAuthDO::getUpdateTime)
|
||||
.orderByDesc(ApiPolicyAuthDO::getId));
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,10 @@ import lombok.Getter;
|
||||
@Getter
|
||||
public enum ApiStepTypeEnum {
|
||||
|
||||
START,
|
||||
HTTP,
|
||||
RPC,
|
||||
SCRIPT,
|
||||
FLOW;
|
||||
END;
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> getAllBasePaths() {
|
||||
Set<String> candidates = new LinkedHashSet<>();
|
||||
candidates.add(basePath);
|
||||
candidates.add(DEFAULT_BASE_PATH);
|
||||
candidates.add(LEGACY_BASE_PATH);
|
||||
return new ArrayList<>(candidates);
|
||||
}
|
||||
|
||||
private List<String> allowedIps = new ArrayList<>();
|
||||
|
||||
private List<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ApiFlowDispatcher> 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<ApiInvocationContext> 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<ApiInvocationContext> 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<ApiGatewayResponse> 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<String, Object> 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<String, Object> body = new HashMap<>();
|
||||
body.put("errorMessage", message);
|
||||
body.put("exception", ex.getClass().getSimpleName());
|
||||
context.setResponseBody(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApiInvocationContext> 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<ApiInvocationContext> 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<ApiStepDefinition> steps;
|
||||
@@ -259,12 +218,5 @@ public class ApiFlowAssembler {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public List<ApiStepDefinition> getSteps() {
|
||||
return steps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.integration.dsl.IntegrationFlow;
|
||||
|
||||
/**
|
||||
* Metadata returned by the assembler for flow registration.
|
||||
* @author chenbowen
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> 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";
|
||||
}
|
||||
}
|
||||
@@ -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<ApiInvocationContext> 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<ApiInvocationContext> 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<ApiGatewayResponse> 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<ApiGatewayResponse> invokeForDebug(ApiGatewayInvokeReqVO reqVO) {
|
||||
Message<?> rawMessage = buildDebugMessage(reqVO);
|
||||
Message<ApiInvocationContext> 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<Object> builder = MessageBuilder.withPayload(payload);
|
||||
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> headers, Map<String, Object> 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<String, Object> 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<String, Object> headers, String headerName) {
|
||||
if (headers == null || !StringUtils.hasText(headerName)) {
|
||||
return null;
|
||||
}
|
||||
for (String key : headers.keySet()) {
|
||||
if (headerName.equalsIgnoreCase(key)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> uriVariables = (Map<String, Object>) 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<String, Object> requestHeaders = (Map<String, Object>) 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<String, Object> 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<String, Object> headers) {
|
||||
if (headers == null) {
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, Object> 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<String, Object> headers) {
|
||||
if (headers == null) {
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, Object> 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<String, Object> target) {
|
||||
try {
|
||||
MultiValueMap<String, String> 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<String, Object> target, boolean override) {
|
||||
source.forEach((rawKey, rawValue) -> {
|
||||
if (!(rawKey instanceof String key) || !StringUtils.hasText(key)) {
|
||||
return;
|
||||
}
|
||||
List<String> 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<String> 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<String> result = new ArrayList<>();
|
||||
for (Object element : (Iterable<?>) value) {
|
||||
result.addAll(normalizeParamValues(element));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (value.getClass().isArray()) {
|
||||
int length = Array.getLength(value);
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<String, Object> source, Map<String, Object> 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<String, Object> headers, String headerName) {
|
||||
if (headers == null || !StringUtils.hasText(headerName)) {
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<org.aopalliance.aop.Advice> 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<Advice> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, ApiTransformDefinition> apiLevelTransforms;
|
||||
|
||||
ApiPolicyAuthDO authPolicy;
|
||||
|
||||
ApiPolicyRateLimitDO rateLimitPolicy;
|
||||
|
||||
ApiFlowPublication publication;
|
||||
|
||||
@@ -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<String, Object> snapshotRequest(ApiInvocationContext context) {
|
||||
Map<String, Object> 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<String, Object> snapshotResponse(ApiInvocationContext context) {
|
||||
Map<String, Object> 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<String, Object> toObjectMap(Map<?, ?> origin) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
origin.forEach((key, value) -> result.put(String.valueOf(key), value));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, String> toStringMap(Map<?, ?> origin) {
|
||||
Map<String, String> 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> T firstNonNull(T... values) {
|
||||
for (T value : values) {
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return 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.");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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] 跳过网关策略迁移,继续使用标准头部令牌认证");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, List<String>> 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<String> 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<String> 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<String> values = additionalHeaders.get(name);
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
return values.get(0);
|
||||
}
|
||||
}
|
||||
return super.getHeader(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaders(String name) {
|
||||
List<String> combined = new ArrayList<>();
|
||||
if (StringUtils.hasText(name)) {
|
||||
List<String> custom = additionalHeaders.get(name);
|
||||
if (!CollectionUtils.isEmpty(custom)) {
|
||||
combined.addAll(custom);
|
||||
}
|
||||
}
|
||||
Enumeration<String> parent = super.getHeaders(name);
|
||||
while (parent.hasMoreElements()) {
|
||||
combined.add(parent.nextElement());
|
||||
}
|
||||
return Collections.enumeration(combined);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getHeaderNames() {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
Enumeration<String> parent = super.getHeaderNames();
|
||||
while (parent.hasMoreElements()) {
|
||||
names.add(parent.nextElement());
|
||||
}
|
||||
names.addAll(additionalHeaders.keySet());
|
||||
return Collections.enumeration(names);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> headers,
|
||||
Map<String, Object> 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<String> 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<String, Object> headers, String headerName, ObjectMapper objectMapper) {
|
||||
if (headers == null || !StringUtils.hasText(headerName)) {
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, Object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object>> 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<String, Object> 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<String, Object> target, HttpServletRequest request) {
|
||||
String queryString = request.getQueryString();
|
||||
if (!StringUtils.hasText(queryString)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
MultiValueMap<String, String> 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<String, Object> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ApiStepHandler> stepHandlers;
|
||||
|
||||
public GenericHandler<ApiInvocationContext> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApiInvocationContext> 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApiInvocationContext> 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<ApiClientCredentialDO> getPage(ApiClientCredentialPageReqVO reqVO);
|
||||
|
||||
Long create(ApiClientCredentialSaveReqVO reqVO);
|
||||
|
||||
void update(ApiClientCredentialSaveReqVO reqVO);
|
||||
|
||||
void delete(Long id);
|
||||
|
||||
ApiClientCredentialDO get(Long id);
|
||||
|
||||
List<ApiClientCredentialDO> listEnabled();
|
||||
|
||||
Optional<ApiClientCredentialDO> findActiveCredential(String appId);
|
||||
}
|
||||
@@ -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<ApiPolicyAuthDO> getPage(ApiPolicyPageReqVO reqVO);
|
||||
|
||||
/**
|
||||
* Fetch all active policies for dropdowns.
|
||||
*/
|
||||
List<ApiPolicyAuthDO> getSimpleList();
|
||||
|
||||
/**
|
||||
* Find policy detail.
|
||||
*/
|
||||
Optional<ApiPolicyAuthDO> get(Long id);
|
||||
|
||||
/**
|
||||
* Create policy definition.
|
||||
*/
|
||||
Long create(ApiPolicySaveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* Update policy definition.
|
||||
*/
|
||||
void update(ApiPolicySaveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* Delete policy definition.
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
}
|
||||
@@ -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<String, Optional<ApiClientCredentialDO>> credentialCache;
|
||||
|
||||
@PostConstruct
|
||||
public void initCache() {
|
||||
credentialCache = Caffeine.newBuilder()
|
||||
.maximumSize(256)
|
||||
.expireAfterWrite(Duration.ofMinutes(5))
|
||||
.build(this::loadCredentialSync);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<ApiClientCredentialDO> 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<ApiClientCredentialDO> listEnabled() {
|
||||
return credentialMapper.selectEnabledList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ApiClientCredentialDO> findActiveCredential(String appId) {
|
||||
if (!StringUtils.hasText(appId)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return credentialCache.get(appId.trim());
|
||||
}
|
||||
|
||||
private Optional<ApiClientCredentialDO> loadCredentialSync(String appId) {
|
||||
Optional<ApiClientCredentialDO> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<ApiTransformDefinition> transforms = convertTransforms(apiTransformMapper.selectByStepId(stepDO.getId()));
|
||||
Map<String, Object> 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<Map<String, Object>>() {});
|
||||
} 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<Integer> 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);
|
||||
}
|
||||
|
||||
@@ -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<ApiPolicyAuthDO> getPage(ApiPolicyPageReqVO reqVO) {
|
||||
return authMapper.selectPage(reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApiPolicyAuthDO> getSimpleList() {
|
||||
return authMapper.selectSimpleList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ApiPolicyAuthDO> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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, "应用标识已存在");
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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"));
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> headers = Map.of();
|
||||
Map<String, Object> queryParams = Map.of("token", " token-999 ");
|
||||
|
||||
String token = GatewayJwtResolver.resolveJwtToken(headers, queryParams, objectMapper);
|
||||
|
||||
assertThat(token).isEqualTo("token-999");
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> 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<String, String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<ApiInvocationContext> 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<String, ?> responseBody = (Map<String, ?>) 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<ApiInvocationContext> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<ApiInvocationContext> 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<String, ?> mappedBody = (Map<String, ?>) 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<ApiInvocationContext> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<ApiStepDO> 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<ApiTransformDO> apiLevelTransforms = apiTransformMapper.selectApiLevelTransforms(definitionId);
|
||||
assertEquals(1, apiLevelTransforms.size());
|
||||
assertEquals("REQUEST_PRE", apiLevelTransforms.get(0).getPhase());
|
||||
assertEquals(0, apiLevelTransforms.size());
|
||||
|
||||
List<ApiTransformDO> stepTransforms = apiTransformMapper.selectByStepId(step.getId());
|
||||
List<ApiTransformDO> 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<ApiStepDO> 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<ApiTransformDO> 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() {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: # 多租户相关配置项
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -134,8 +134,8 @@ zt:
|
||||
permit-all_urls:
|
||||
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
|
||||
swagger:
|
||||
title: 管理后台
|
||||
description: 提供管理员管理的所有功能
|
||||
title: 小程序
|
||||
description: 提供小程序功能
|
||||
version: ${zt.info.version}
|
||||
tenant: # 多租户相关配置项
|
||||
enable: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Boolean> initializeDeptCodes() {
|
||||
deptService.initializeDeptCodes();
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("delete")
|
||||
@Operation(summary = "删除部门")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<DeptDO> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定父部门下编码最大的子部门
|
||||
*
|
||||
* @param parentId 父部门ID
|
||||
* @return 编码最大的子部门
|
||||
*/
|
||||
default DeptDO selectLastChildByCode(Long parentId) {
|
||||
return selectOne(new LambdaQueryWrapper<DeptDO>()
|
||||
.eq(DeptDO::getParentId, parentId)
|
||||
.isNotNull(DeptDO::getCode)
|
||||
.orderByDesc(DeptDO::getCode)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门编码查询部门
|
||||
*
|
||||
|
||||
@@ -152,4 +152,9 @@ public interface DeptService {
|
||||
* @return 公司列表
|
||||
*/
|
||||
List<DeptDO> getAllCompanyList();
|
||||
|
||||
/**
|
||||
* 按照新的编码规则初始化全部部门编码
|
||||
*/
|
||||
void initializeDeptCodes();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user