diff --git a/sql/dm/patch.sql b/sql/dm/patch.sql index d20ce6f3..e8b3202a 100644 --- a/sql/dm/patch.sql +++ b/sql/dm/patch.sql @@ -7,6 +7,12 @@ ALTER TABLE infra_file ADD aes_iv VARCHAR(128); COMMENT ON COLUMN infra_file.aes_iv IS 'AES加密时的随机IV(Base64编码)'; +-- 3. Databus 客户端凭证新增匿名访问配置(DM8) +ALTER TABLE databus_api_client_credential ADD allow_anonymous BIT DEFAULT '0' NOT NULL; +ALTER TABLE databus_api_client_credential ADD anonymous_user_id BIGINT; +COMMENT ON COLUMN databus_api_client_credential.allow_anonymous IS '是否允许匿名访问'; +COMMENT ON COLUMN databus_api_client_credential.anonymous_user_id IS '匿名访问固定用户'; + -- 3 业务附件统一管理 DROP TABLE IF EXISTS infra_bsn_file; CREATE TABLE infra_bsn_file ( diff --git a/sql/dm/ruoyi-vue-pro-dm8.sql b/sql/dm/ruoyi-vue-pro-dm8.sql index 79d17b9d..24ef2fb7 100644 --- a/sql/dm/ruoyi-vue-pro-dm8.sql +++ b/sql/dm/ruoyi-vue-pro-dm8.sql @@ -656,6 +656,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (36, 3, '本部门数据权限', '3', 'system_data_scope', 0, '', '', '本部门数据权限', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:47:16', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (37, 4, '本部门及以下数据权限', '4', 'system_data_scope', 0, '', '', '本部门及以下数据权限', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:47:21', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (38, 5, '仅本人数据权限', '5', 'system_data_scope', 0, '', '', '仅本人数据权限', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:47:23', '0'); +INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (200010, 6, '公司及所属部门数据权限', '6', 'system_data_scope', 0, '', '', '公司及所属部门数据权限', 'admin', '2021-01-05 17:03:48', '', '2025-10-24 00:00:00', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (39, 0, '成功', '0', 'system_login_result', 0, 'success', '', '登陆结果 - 成功', '', '2021-01-18 06:17:36', '1', '2022-02-16 13:23:49', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (40, 10, '账号或密码不正确', '10', 'system_login_result', 0, 'primary', '', '登陆结果 - 账号或密码不正确', '', '2021-01-18 06:17:54', '1', '2022-02-16 13:24:27', '0'); INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (41, 20, '用户被禁用', '20', 'system_login_result', 0, 'warning', '', '登陆结果 - 用户被禁用', '', '2021-01-18 06:17:54', '1', '2022-02-16 13:23:57', '0'); diff --git a/sql/dm/统一对外网关_20251010.sql b/sql/dm/统一对外网关_20251010.sql index 55f7f82f..b334f638 100644 --- a/sql/dm/统一对外网关_20251010.sql +++ b/sql/dm/统一对外网关_20251010.sql @@ -198,7 +198,7 @@ COMMENT ON COLUMN databus_api_transform.update_time IS '更新时间'; COMMENT ON COLUMN databus_api_transform.deleted IS '逻辑删除标记'; - -- 统一外部网关 - 客户端凭证表(DM8) +-- 统一外部网关 - 客户端凭证表(DM8) -- 可重复执行的建表脚本,执行前请备份历史数据 DROP TABLE IF EXISTS databus_api_client_credential; @@ -212,6 +212,8 @@ CREATE TABLE databus_api_client_credential ( signature_type VARCHAR(32) NOT NULL, enabled BIT DEFAULT '1' NOT NULL, remark VARCHAR(255), + allow_anonymous BIT DEFAULT '0' NOT NULL, + anonymous_user_id BIGINT, creator VARCHAR(64) DEFAULT '' NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updater VARCHAR(64) DEFAULT '' NOT NULL, @@ -231,6 +233,8 @@ 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.allow_anonymous IS '是否允许匿名访问'; +COMMENT ON COLUMN databus_api_client_credential.anonymous_user_id 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 '更新者'; diff --git a/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java index 79e405d2..6fb4808c 100644 --- a/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java +++ b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/security/CryptoSignatureUtils.java @@ -9,11 +9,7 @@ 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; +import java.util.*; /** * 通用的签名、加解密工具类 @@ -26,7 +22,7 @@ public final class CryptoSignatureUtils { public static final String SIGNATURE_TYPE_SHA256 = "SHA256"; private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding"; - private static final String SIGNATURE_FIELD = "signature"; + public static final String SIGNATURE_FIELD = "signature"; private CryptoSignatureUtils() { } diff --git a/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/validation/Password.java b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/validation/Password.java new file mode 100644 index 00000000..61442516 --- /dev/null +++ b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/validation/Password.java @@ -0,0 +1,31 @@ +package com.zt.plat.framework.common.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +/** + * 密码复杂度校验注解,要求至少包含大小写字母、数字、特殊字符中的三种。 + * @author chenbowen + */ +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint(validatedBy = PasswordValidator.class) +public @interface Password { + + String message() default "密码必须包含大写字母、小写字母、数字、特殊字符中的至少三种"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/validation/PasswordValidator.java b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/validation/PasswordValidator.java new file mode 100644 index 00000000..5b401658 --- /dev/null +++ b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/validation/PasswordValidator.java @@ -0,0 +1,38 @@ +package com.zt.plat.framework.common.validation; + +import cn.hutool.core.util.StrUtil; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * 密码复杂度校验:至少命中以下类别中的三类:大写字母、小写字母、数字、特殊字符。 + */ +public class PasswordValidator implements ConstraintValidator { + + @Override + public void initialize(Password constraintAnnotation) { + // no-op + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (StrUtil.isBlank(value)) { + // 空值交由 @NotEmpty 等注解处理;在无需修改密码时视为空密码通过 + return true; + } + int categories = 0; + if (value.matches(".*[A-Z].*")) { + categories++; + } + if (value.matches(".*[a-z].*")) { + categories++; + } + if (value.matches(".*[0-9].*")) { + categories++; + } + if (value.matches(".*[^A-Za-z0-9].*")) { + categories++; + } + return categories >= 3; + } +} diff --git a/zt-framework/zt-common/src/test/java/com/zt/plat/framework/common/validation/PasswordValidatorTest.java b/zt-framework/zt-common/src/test/java/com/zt/plat/framework/common/validation/PasswordValidatorTest.java new file mode 100644 index 00000000..bee1cf51 --- /dev/null +++ b/zt-framework/zt-common/src/test/java/com/zt/plat/framework/common/validation/PasswordValidatorTest.java @@ -0,0 +1,31 @@ +package com.zt.plat.framework.common.validation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PasswordValidatorTest { + + private final PasswordValidator validator = new PasswordValidator(); + + @Test + void shouldAcceptBlankPassword() { + assertTrue(validator.isValid(null, null)); + assertTrue(validator.isValid("", null)); + assertTrue(validator.isValid(" ", null)); + } + + @Test + void shouldRejectInsufficientComplexity() { + assertFalse(validator.isValid("abcdef", null)); + assertFalse(validator.isValid("ABCDEF", null)); + assertFalse(validator.isValid("ABC123", null)); + } + + @Test + void shouldAcceptComplexPassword() { + assertTrue(validator.isValid("Abc123!", null)); + assertTrue(validator.isValid("1a#BCdef", null)); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiClientCredentialController.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiClientCredentialController.java index 3682b7d8..4c9a41be 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiClientCredentialController.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiClientCredentialController.java @@ -8,6 +8,7 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClie 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.ApiAnonymousUserService; import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; import static com.zt.plat.framework.common.pojo.CommonResult.success; +import static org.springframework.util.CollectionUtils.isEmpty; @Tag(name = "管理后台 - API 客户端凭证") @RestController @@ -35,19 +37,24 @@ import static com.zt.plat.framework.common.pojo.CommonResult.success; public class ApiClientCredentialController { private final ApiClientCredentialService credentialService; + private final ApiAnonymousUserService anonymousUserService; @GetMapping("/page") @Operation(summary = "分页查询客户端凭证") public CommonResult> page(ApiClientCredentialPageReqVO reqVO) { PageResult page = credentialService.getPage(reqVO); - return success(ApiClientCredentialConvert.INSTANCE.convertPage(page)); + PageResult respPage = ApiClientCredentialConvert.INSTANCE.convertPage(page); + populateAnonymousInfo(respPage.getList()); + return success(respPage); } @GetMapping("/get") @Operation(summary = "查询凭证详情") public CommonResult get(@RequestParam("id") Long id) { ApiClientCredentialDO credential = credentialService.get(id); - return success(ApiClientCredentialConvert.INSTANCE.convert(credential)); + ApiClientCredentialRespVO respVO = ApiClientCredentialConvert.INSTANCE.convert(credential); + populateAnonymousInfo(List.of(respVO)); + return success(respVO); } @PostMapping("/create") @@ -76,4 +83,14 @@ public class ApiClientCredentialController { List list = credentialService.listEnabled(); return success(ApiClientCredentialConvert.INSTANCE.convertSimpleList(list)); } + + private void populateAnonymousInfo(List list) { + if (isEmpty(list)) { + return; + } + list.stream() + .filter(item -> Boolean.TRUE.equals(item.getAllowAnonymous()) && item.getAnonymousUserId() != null) + .forEach(item -> anonymousUserService.find(item.getAnonymousUserId()) + .ifPresent(details -> item.setAnonymousUserNickname(details.getNickname()))); + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayController.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayController.java index 5f451796..191b5142 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayController.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayController.java @@ -5,6 +5,7 @@ import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiDefinition 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.ApiGatewayExecutionService; +import com.zt.plat.module.databus.framework.integration.gateway.core.IntegrationFlowManager; import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +28,7 @@ public class ApiGatewayController { private final ApiGatewayExecutionService executionService; private final ApiDefinitionService apiDefinitionService; + private final IntegrationFlowManager integrationFlowManager; @PostMapping(value = "/invoke", consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "测试调用 API 编排") @@ -43,4 +45,12 @@ public class ApiGatewayController { return success(definitions); } + @PostMapping("/cache/refresh") + @Operation(summary = "刷新 API 缓存") + public CommonResult refreshCache() { + apiDefinitionService.refreshAllCache(); + integrationFlowManager.refreshAll(); + return success(Boolean.TRUE); + } + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialRespVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialRespVO.java index ceb9d621..33e2dbe7 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialRespVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialRespVO.java @@ -33,6 +33,15 @@ public class ApiClientCredentialRespVO { @Schema(description = "备注", example = "默认应用凭证") private String remark; + @Schema(description = "允许匿名访问", example = "false") + private Boolean allowAnonymous; + + @Schema(description = "匿名访问固定用户 ID", example = "1024") + private Long anonymousUserId; + + @Schema(description = "匿名访问固定用户昵称", example = "张三") + private String anonymousUserNickname; + @Schema(description = "创建时间") private LocalDateTime createTime; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSaveReqVO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSaveReqVO.java index 15765118..11043ac5 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSaveReqVO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/controller/admin/gateway/vo/credential/ApiClientCredentialSaveReqVO.java @@ -38,4 +38,11 @@ public class ApiClientCredentialSaveReqVO { @Schema(description = "备注", example = "默认应用凭证") private String remark; + @Schema(description = "允许匿名访问", example = "false") + @NotNull(message = "匿名访问标识不能为空") + private Boolean allowAnonymous; + + @Schema(description = "匿名访问固定用户 ID", example = "1024") + private Long anonymousUserId; + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiClientCredentialDO.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiClientCredentialDO.java index 76d0ad44..0bf5134f 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiClientCredentialDO.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/dal/dataobject/gateway/ApiClientCredentialDO.java @@ -34,4 +34,8 @@ public class ApiClientCredentialDO extends BaseDO { private String remark; + private Boolean allowAnonymous; + + private Long anonymousUserId; + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/ApiGatewayProperties.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/ApiGatewayProperties.java index 036f41d3..4ed071a7 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/ApiGatewayProperties.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/config/ApiGatewayProperties.java @@ -17,6 +17,10 @@ import java.util.Set; @ConfigurationProperties(prefix = "databus.api-portal") public class ApiGatewayProperties { + public static String APP_ID_HEADER = "ZT-App-Id"; + public static String TIMESTAMP_HEADER = "ZT-Timestamp"; + public static String NONCE_HEADER = "ZT-Nonce"; + public static String SIGNATURE_HEADER = "ZT-Signature"; public static final String DEFAULT_BASE_PATH = "/admin-api/databus/api/portal"; public static final String LEGACY_BASE_PATH = "/databus/api/portal"; @@ -62,14 +66,6 @@ public class ApiGatewayProperties { 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; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java new file mode 100644 index 00000000..9a32813f --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java @@ -0,0 +1,24 @@ +package com.zt.plat.module.databus.framework.integration.gateway.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class GatewayWebClientConfiguration { + + private final int maxInMemorySize; + + public GatewayWebClientConfiguration( + @Value("${databus.gateway.web-client.max-in-memory-size:2097152}") int maxInMemorySize) { + this.maxInMemorySize = maxInMemorySize; + } + + @Bean + public WebClientCustomizer gatewayWebClientCustomizer() { + return builder -> builder.codecs(configurer -> + configurer.defaultCodecs().maxInMemorySize(maxInMemorySize)); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java index 34b47545..0c19c944 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java @@ -3,11 +3,17 @@ 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.json.JsonUtils; import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; import com.zt.plat.framework.common.util.servlet.ServletUtils; +import com.zt.plat.framework.security.core.LoginUser; +import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; +import com.zt.plat.framework.tenant.core.context.TenantContextHolder; +import com.zt.plat.framework.web.core.util.WebFrameworkUtils; 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.ApiAnonymousUserService; import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -25,6 +31,7 @@ import org.springframework.web.util.ContentCachingResponseWrapper; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; +import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -32,6 +39,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import static com.zt.plat.framework.common.util.security.CryptoSignatureUtils.SIGNATURE_FIELD; +import static com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties.*; + /** * 对进入网关的请求统一执行 IP 校验、报文签名、加解密与防重复校验。 */ @@ -43,6 +53,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { private final ApiGatewayProperties properties; private final StringRedisTemplate stringRedisTemplate; private final ApiClientCredentialService credentialService; + private final ApiAnonymousUserService anonymousUserService; private final ObjectMapper objectMapper; private final AntPathMatcher pathMatcher = new AntPathMatcher(); private static final TypeReference> MAP_TYPE = new TypeReference<>() {}; @@ -51,6 +62,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String pathWithinApplication = pathWithinApplication(request); + // 仅处理配置的 API 门户路径,不符合的请求直接放行 boolean matchesPortalPath = properties.getAllBasePaths() .stream() .map(this::normalizeBasePath) @@ -59,6 +71,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); return; } + // 校验访问 IP 是否落在允许范围内 if (!isIpAllowed(request)) { log.warn("[API-PORTAL] 拦截来自 IP {} 访问 {} 的请求", request.getRemoteAddr(), pathWithinApplication); response.sendError(HttpStatus.FORBIDDEN.value(), "IP 禁止访问"); @@ -72,28 +85,46 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { } try { Long tenantId = resolveTenantId(request); - String appId = requireHeader(request, security.getAppIdHeader(), "缺少应用标识"); + // 从请求头解析 appId 并加载客户端凭证,包含匿名访问配置 + String appId = requireHeader(request, APP_ID_HEADER, "缺少应用标识"); credential = credentialService.findActiveCredential(appId) .orElseThrow(() -> new SecurityValidationException(HttpStatus.UNAUTHORIZED, "应用凭证不存在或已禁用")); + boolean allowAnonymous = Boolean.TRUE.equals(credential.getAllowAnonymous()); + ApiAnonymousUserService.AnonymousUserDetails anonymousDetails = null; + if (allowAnonymous) { + Long anonymousUserId = credential.getAnonymousUserId(); + if (anonymousUserId == null) { + throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "匿名访问未配置固定用户"); + } + anonymousDetails = anonymousUserService.find(anonymousUserId) + .orElseThrow(() -> new SecurityValidationException(HttpStatus.UNAUTHORIZED, "匿名访问固定用户不可用")); + } - String timestampHeader = requireHeader(request, security.getTimestampHeader(), "缺少时间戳"); + String timestampHeader = requireHeader(request, TIMESTAMP_HEADER, "缺少时间戳"); + // 校验时间戳与随机数,防止请求被重放 validateTimestamp(timestampHeader, security); - String nonce = requireHeader(request, security.getNonceHeader(), "缺少随机数"); + String nonce = requireHeader(request, NONCE_HEADER, "缺少随机数"); if (nonce.length() < 8) { throw new SecurityValidationException(HttpStatus.BAD_REQUEST, "随机数长度不足"); } - String signature = requireHeader(request, security.getSignatureHeader(), "缺少签名"); + String signature = requireHeader(request, SIGNATURE_HEADER, "缺少签名"); byte[] originalBody = StreamUtils.copyToByteArray(request.getInputStream()); + // 尝试按凭证配置解密请求体,并构建签名载荷进行校验 byte[] decryptedBody = decryptRequestBody(originalBody, credential, security); - verifySignature(request, decryptedBody, signature, credential, security); + verifySignature(request, decryptedBody, signature, credential, security, appId, timestampHeader); ensureNonce(tenantId, appId, nonce, security); + // 使用可重复读取的请求包装,供后续过滤器继续消费 CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody); if (StringUtils.hasText(request.getCharacterEncoding())) { securedRequest.setCharacterEncoding(request.getCharacterEncoding()); } - propagateJwtToken(request, securedRequest); + if (allowAnonymous) { + applyAnonymousLogin(securedRequest, anonymousDetails); + } else { + propagateJwtToken(request, securedRequest); + } ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); try { filterChain.doFilter(securedRequest, responseWrapper); @@ -213,11 +244,15 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { byte[] decryptedBody, String signature, ApiClientCredentialDO credential, - ApiGatewayProperties.Security security) { + ApiGatewayProperties.Security security, + String appId, + String timestampHeader) { Map signaturePayload = new LinkedHashMap<>(); mergeQueryParameters(signaturePayload, request); mergeBodyParameters(signaturePayload, decryptedBody); - signaturePayload.put("signature", signature); + signaturePayload.put(APP_ID_HEADER, appId); + signaturePayload.put(TIMESTAMP_HEADER, timestampHeader); + signaturePayload.put(SIGNATURE_FIELD, signature); String signatureType = resolveSignatureType(credential, security); try { @@ -331,12 +366,16 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { if (!security.isEncryptResponse()) { return; } + byte[] plainBody = responseWrapper.getContentAsByteArray(); + if (plainBody.length == 0) { + // 空响应无需加密,直接返回避免因密钥缺失导致异常 + 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(); @@ -372,6 +411,33 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { securedRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); } + /** + * 匿名访问场景下,将固定用户写入安全上下文并同步到请求头,确保后续业务链路能够识别身份。 + */ + private void applyAnonymousLogin(CachedBodyHttpServletRequest securedRequest, + ApiAnonymousUserService.AnonymousUserDetails anonymousDetails) { + LoginUser loginUser = anonymousDetails.getLoginUser(); + SecurityFrameworkUtils.setLoginUser(loginUser, securedRequest); + Long tenantId = loginUser.getTenantId(); + // 设置租户标识到请求头与上下文 + securedRequest.setHeader(WebFrameworkUtils.HEADER_TENANT_ID, tenantId != null ? tenantId.toString() : null); + TenantContextHolder.setTenantId(tenantId); + try { + String serialized = JsonUtils.toJsonString(loginUser); + String encoded = URLEncoder.encode(serialized, StandardCharsets.UTF_8); + securedRequest.setHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER, encoded); + } catch (Exception ex) { + log.warn("[API-PORTAL] 序列化匿名访问用户失败", ex); + } + securedRequest.removeHeader(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN); + securedRequest.removeHeader(HttpHeaders.AUTHORIZATION); + anonymousUserService.issueAccessToken(anonymousDetails) + .ifPresent(token -> { + securedRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); + securedRequest.setHeader(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, token); + }); + } + private static final class SecurityValidationException extends RuntimeException { private final HttpStatus status; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/rpc/config/RpcConfiguration.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/rpc/config/RpcConfiguration.java new file mode 100644 index 00000000..7e83f105 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/rpc/config/RpcConfiguration.java @@ -0,0 +1,14 @@ +package com.zt.plat.module.databus.framework.rpc.config; + +import com.zt.plat.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; +import com.zt.plat.module.system.api.user.AdminUserApi; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +/** + * Databus 模块的 RPC 配置,开启所需的 Feign 客户端。 + */ +@Configuration(value = "databusRpcConfiguration", proxyBeanMethods = false) +@EnableFeignClients(clients = {AdminUserApi.class, OAuth2TokenCommonApi.class}) +public class RpcConfiguration { +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java new file mode 100644 index 00000000..788b0498 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java @@ -0,0 +1,150 @@ +package com.zt.plat.module.databus.service.gateway; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.zt.plat.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; +import com.zt.plat.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO; +import com.zt.plat.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO; +import com.zt.plat.framework.common.enums.CommonStatusEnum; +import com.zt.plat.framework.common.enums.UserTypeEnum; +import com.zt.plat.framework.security.core.LoginUser; +import com.zt.plat.framework.tenant.core.aop.TenantIgnore; +import com.zt.plat.module.system.api.user.AdminUserApi; +import com.zt.plat.module.system.api.user.dto.AdminUserRespDTO; +import com.zt.plat.module.system.enums.oauth2.OAuth2ClientConstants; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static com.zt.plat.framework.security.core.LoginUser.INFO_KEY_NICKNAME; +import static com.zt.plat.framework.security.core.LoginUser.INFO_KEY_TENANT_ID; + +/** + * 缓存并提供匿名访问所需的固定用户信息,包含对应的 LoginUser 快照。 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class ApiAnonymousUserService { + + private final AdminUserApi adminUserApi; + private final OAuth2TokenCommonApi oauth2TokenApi; + + private LoadingCache> cache; + + @PostConstruct + void init() { + cache = Caffeine.newBuilder() + .maximumSize(256) + .expireAfterWrite(Duration.ofMinutes(5)) + .build(this::loadUserDetails); + } + + public Optional find(Long userId) { + if (userId == null) { + return Optional.empty(); + } + return cache.get(userId); + } + + public AnonymousUserDetails getOrThrow(Long userId) { + return find(userId).orElseThrow(() -> new IllegalStateException("匿名访问固定用户不存在或不可用")); + } + + public void invalidate(Long userId) { + if (userId != null) { + cache.invalidate(userId); + } + } + + private Optional loadUserDetails(Long userId) { + AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); + if (user == null) { + return Optional.empty(); + } + if (!CommonStatusEnum.isEnable(user.getStatus())) { + return Optional.empty(); + } + LoginUser loginUser = buildLoginUser(user); + return Optional.of(new AnonymousUserDetails(user.getId(), user.getNickname(), loginUser)); + } + + private LoginUser buildLoginUser(AdminUserRespDTO user) { + LoginUser loginUser = new LoginUser(); + loginUser.setId(user.getId()); + loginUser.setUserType(UserTypeEnum.ADMIN.getValue()); + loginUser.setTenantId(user.getTenantId()); + loginUser.setVisitTenantId(user.getTenantId()); + if (!CollectionUtils.isEmpty(user.getDeptIds())) { + loginUser.setVisitDeptId(user.getDeptIds().get(0)); + } + Map info = new HashMap<>(2); + if (user.getNickname() != null) { + info.put(INFO_KEY_NICKNAME, user.getNickname()); + } + if (user.getTenantId() != null) { + info.put(INFO_KEY_TENANT_ID, String.valueOf(user.getTenantId())); + } + loginUser.setInfo(info); + loginUser.setScopes(Collections.emptyList()); + return loginUser; + } + + @TenantIgnore + public Optional issueAccessToken(AnonymousUserDetails details) { + if (details == null) { + return Optional.empty(); + } + try { + OAuth2AccessTokenCreateReqDTO req = buildAccessTokenRequest(details); + OAuth2AccessTokenRespDTO resp = oauth2TokenApi.createAccessToken(req).getCheckedData(); + if (resp == null || !StringUtils.hasText(resp.getAccessToken())) { + log.warn("[ANONYMOUS] 获取用户 {} 的访问令牌失败: 响应为空", details.getUserId()); + return Optional.empty(); + } + return Optional.of(resp.getAccessToken()); + } catch (Exception ex) { + log.error("[ANONYMOUS] 获取用户 {} 的访问令牌时发生异常", details.getUserId(), ex); + return Optional.empty(); + } + } + + private OAuth2AccessTokenCreateReqDTO buildAccessTokenRequest(AnonymousUserDetails details) { + OAuth2AccessTokenCreateReqDTO req = new OAuth2AccessTokenCreateReqDTO(); + req.setUserId(details.getUserId()); + Integer userType = Optional.ofNullable(details.getLoginUser()) + .map(LoginUser::getUserType) + .orElse(UserTypeEnum.ADMIN.getValue()); + req.setUserType(userType); + req.setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT); + if (details.getLoginUser() != null && !CollectionUtils.isEmpty(details.getLoginUser().getScopes())) { + req.setScopes(details.getLoginUser().getScopes()); + } else { + req.setScopes(Collections.emptyList()); + } + return req; + } + + @Getter + public static final class AnonymousUserDetails { + private final Long userId; + private final String nickname; + private final LoginUser loginUser; + + public AnonymousUserDetails(Long userId, String nickname, LoginUser loginUser) { + this.userId = userId; + this.nickname = nickname; + this.loginUser = loginUser; + } + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionService.java index 85462294..e745c6bf 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionService.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionService.java @@ -29,6 +29,11 @@ public interface ApiDefinitionService { */ Optional refresh(String apiCode, String version); + /** + * Evict all cached API definitions, forcing rebuild on next access. + */ + void refreshAllCache(); + /** * Lookup API definition aggregate by primary key. */ diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiClientCredentialServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiClientCredentialServiceImpl.java index 76e32a30..415684fb 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiClientCredentialServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiClientCredentialServiceImpl.java @@ -9,6 +9,7 @@ import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClie 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.ApiAnonymousUserService; import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; @@ -24,6 +25,8 @@ 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; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_ANONYMOUS_USER_INVALID; +import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_ANONYMOUS_USER_REQUIRED; @Slf4j @Service @@ -31,6 +34,7 @@ import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErro public class ApiClientCredentialServiceImpl implements ApiClientCredentialService { private final ApiClientCredentialMapper credentialMapper; + private final ApiAnonymousUserService anonymousUserService; private LoadingCache> credentialCache; @@ -51,10 +55,17 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic @Transactional(rollbackFor = Exception.class) public Long create(ApiClientCredentialSaveReqVO reqVO) { ensureAppIdUnique(reqVO.getAppId(), null); + normalizeAnonymousSettings(reqVO); ApiClientCredentialDO credential = BeanUtils.toBean(reqVO, ApiClientCredentialDO.class); credential.setId(null); credential.setDeleted(Boolean.FALSE); + if (credential.getAllowAnonymous() == null) { + credential.setAllowAnonymous(Boolean.FALSE); + } + if (Boolean.FALSE.equals(credential.getAllowAnonymous())) { + credential.setAnonymousUserId(null); + } credentialMapper.insert(credential); invalidateCache(credential.getAppId()); return credential.getId(); @@ -65,11 +76,22 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic public void update(ApiClientCredentialSaveReqVO reqVO) { ApiClientCredentialDO existing = ensureExists(reqVO.getId()); ensureAppIdUnique(reqVO.getAppId(), existing.getId()); + normalizeAnonymousSettings(reqVO); ApiClientCredentialDO updateObj = BeanUtils.toBean(reqVO, ApiClientCredentialDO.class); + if (updateObj.getAllowAnonymous() == null) { + updateObj.setAllowAnonymous(Boolean.FALSE); + } + if (Boolean.FALSE.equals(updateObj.getAllowAnonymous())) { + updateObj.setAnonymousUserId(null); + } credentialMapper.updateById(updateObj); invalidateCache(existing.getAppId()); invalidateCache(updateObj.getAppId()); + if (!Objects.equals(existing.getAnonymousUserId(), updateObj.getAnonymousUserId())) { + anonymousUserService.invalidate(existing.getAnonymousUserId()); + anonymousUserService.invalidate(updateObj.getAnonymousUserId()); + } } @Override @@ -78,6 +100,7 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic ApiClientCredentialDO existing = ensureExists(id); credentialMapper.deleteById(id); invalidateCache(existing.getAppId()); + anonymousUserService.invalidate(existing.getAnonymousUserId()); } @Override @@ -130,4 +153,18 @@ public class ApiClientCredentialServiceImpl implements ApiClientCredentialServic } credentialCache.invalidate(appId.trim()); } + + private void normalizeAnonymousSettings(ApiClientCredentialSaveReqVO reqVO) { + if (Boolean.TRUE.equals(reqVO.getAllowAnonymous())) { + if (reqVO.getAnonymousUserId() == null) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_ANONYMOUS_USER_REQUIRED); + } + if (anonymousUserService.find(reqVO.getAnonymousUserId()).isEmpty()) { + throw ServiceExceptionUtil.exception(API_CREDENTIAL_ANONYMOUS_USER_INVALID); + } + } else { + reqVO.setAllowAnonymous(Boolean.FALSE); + reqVO.setAnonymousUserId(null); + } + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java index 16eaa61b..894ee3cb 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/ApiDefinitionServiceImpl.java @@ -38,6 +38,7 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.time.Duration; @@ -102,6 +103,12 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { return findByCodeAndVersion(apiCode, version); } + @Override + public void refreshAllCache() { + definitionCache.invalidateAll(); + clearRedisCacheForTenant(TenantContextHolder.getTenantId()); + } + @Override public Optional findById(Long id) { return Optional.ofNullable(apiDefinitionMapper.selectById(id)) @@ -205,11 +212,28 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { } } + private void clearRedisCacheForTenant(Long tenantId) { + String tenantPart = tenantId == null ? "global" : tenantId.toString(); + String pattern = REDIS_CACHE_PREFIX + tenantPart + ":*"; + try { + Set keys = stringRedisTemplate.keys(pattern); + if (CollectionUtils.isEmpty(keys)) { + return; + } + stringRedisTemplate.delete(keys); + } catch (DataAccessException ex) { + log.warn("批量删除 Redis 中匹配 {} 的 API 定义聚合失败", pattern, ex); + } + } + private String buildCacheKey(String apiCode, String version) { Long tenantId = TenantContextHolder.getTenantId(); return buildCacheKeyForTenant(tenantId, apiCode, version); } + /** + * 构建包含步骤、变换、策略等元数据的聚合对象,供缓存与运行时直接使用。 + */ private ApiDefinitionAggregate buildAggregate(ApiDefinitionDO definition) { List stepDOS = apiStepMapper.selectByApiId(definition.getId()); List stepDefinitions = new ArrayList<>(stepDOS.size()); @@ -313,6 +337,9 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { } } + /** + * 顺序持久化步骤定义,并针对开始/结束节点清理不必要配置避免脏数据。 + */ private void persistSteps(Long apiId, List steps) { if (CollUtil.isEmpty(steps)) { return; @@ -323,14 +350,7 @@ 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)) { + if (isStartStep(stepVO) || isEndStep(stepVO)) { stepDO.setParallelGroup(null); stepDO.setTargetEndpoint(null); stepDO.setFallbackStrategy(null); @@ -380,6 +400,9 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService { .ifPresent(definition -> { throw ServiceExceptionUtil.exception(API_DEFINITION_DUPLICATE); }); } + /** + * 校验步骤编排的拓扑约束,确保开始/结束节点唯一且位置正确。 + */ private void validateStructure(ApiDefinitionSaveReqVO reqVO) { if (CollUtil.isEmpty(reqVO.getSteps())) { throw ServiceExceptionUtil.exception(API_DEFINITION_STEP_EMPTY); diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java index d5e1e815..a82a4a0e 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/impl/GatewayServiceErrorCodeConstants.java @@ -52,5 +52,8 @@ public interface GatewayServiceErrorCodeConstants { 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, "应用标识已存在"); + ErrorCode API_STEP_MAPPING_CONFIG_INVALID = new ErrorCode(1_010_000_046, "步骤映射配置 JSON 非法"); + ErrorCode API_CREDENTIAL_ANONYMOUS_USER_REQUIRED = new ErrorCode(1_010_000_047, "启用匿名访问时必须指定固定用户"); + ErrorCode API_CREDENTIAL_ANONYMOUS_USER_INVALID = new ErrorCode(1_010_000_048, "匿名访问固定用户不存在或已被禁用"); } diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java index b4c6207e..6a3768f4 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java @@ -3,12 +3,15 @@ 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.framework.integration.config.ApiGatewayProperties; +import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; 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.data.redis.core.StringRedisTemplate; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestPropertySource; @@ -25,7 +28,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @WebMvcTest(ApiGatewayController.class) @AutoConfigureMockMvc(addFilters = false) @TestPropertySource(properties = { - "spring.config.import=optional:", "spring.cloud.nacos.config.enabled=false", "spring.cloud.nacos.discovery.enabled=false" }) @@ -40,6 +42,15 @@ class ApiGatewayControllerTest { @MockBean private ApiDefinitionService apiDefinitionService; + @MockBean + private ApiGatewayProperties apiGatewayProperties; + + @MockBean + private StringRedisTemplate stringRedisTemplate; + + @MockBean + private ApiClientCredentialService apiClientCredentialService; + @Test void invokeShouldReturnGatewayEnvelope() throws Exception { ApiGatewayResponse response = ApiGatewayResponse.builder() diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java new file mode 100644 index 00000000..67b7cb7f --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java @@ -0,0 +1,258 @@ +package com.zt.plat.module.databus.framework.integration.gateway.sample; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; + +import java.io.PrintStream; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; + +/** + * 可直接运行的示例,演示如何使用 appId=test 与对应密钥调用本地 Databus API。 + */ +public final class DatabusApiInvocationExample { + + public static final String TIMESTAMP = Long.toString(System.currentTimeMillis()); +// private static final String APP_ID = "ztmy"; +// private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; + private static final String APP_ID = "test"; + private static final String APP_SECRET = "RSYtKXrXPLMy3oeh0cOro6QCioRUgqfnKCkDkNq78sI="; + private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES; +// private static final String TARGET_API = "http://127.0.0.1:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; + private static final String TARGET_API = "http://127.0.0.1:48080/admin-api/databus/api/portal/test11111/233"; + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(5)) + .build(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final PrintStream OUT = buildConsolePrintStream(); + public static final String ZT_APP_ID = "ZT-App-Id"; + public static final String ZT_TIMESTAMP = "ZT-Timestamp"; + public static final String ZT_NONCE = "ZT-Nonce"; + public static final String ZT_SIGNATURE = "ZT-Signature"; + public static final String ZT_AUTH_TOKEN = "ZT-Auth-Token"; + public static final String CONTENT_TYPE = "Content-Type"; + + private DatabusApiInvocationExample() { + } + + public static void main(String[] args) throws Exception { + OUT.println("=== GET 请求示例 ==="); + executeGetExample(); +// OUT.println(); +// OUT.println("=== POST 请求示例 ==="); +// executePostExample(); + } + + private static void executeGetExample() throws Exception { + Map queryParams = new LinkedHashMap<>(); + queryParams.put("businessCode", "waybillUnLoadingImage"); + queryParams.put("fileId", "1979463299195412481"); + String signature = generateSignature(queryParams, Map.of()); + URI requestUri = buildUri(TARGET_API, queryParams); + String nonce = randomNonce(); + + HttpRequest request = HttpRequest.newBuilder(requestUri) + .timeout(Duration.ofSeconds(10)) + .header(ZT_APP_ID, APP_ID) + .header(ZT_TIMESTAMP, TIMESTAMP) + .header(ZT_NONCE, nonce) + .header(ZT_SIGNATURE, signature) +// .header("ZT-Auth-Token", "a75c0ea94c7f4a88b86b60bbc0b432c3") + .GET() + .build(); + + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + printResponse(response); + } + + private static void executePostExample() throws Exception { + Map queryParams = new LinkedHashMap<>(); + + LinkedHashMap bodyParams = new LinkedHashMap<>(); + bodyParams.put("businessCode", "waybillUnLoadingImage"); + bodyParams.put("fileId", "1979463299195412481"); + + LinkedHashMap extra = new LinkedHashMap<>(); + extra.put("remark", "demo invocation"); + extra.put("timestamp", System.currentTimeMillis()); + bodyParams.put("extra", extra); + + String signature = generateSignature(queryParams, bodyParams); + URI requestUri = buildUri(TARGET_API, queryParams); + String nonce = randomNonce(); + String bodyJson = OBJECT_MAPPER.writeValueAsString(bodyParams); + String cipherBody = encryptPayload(bodyJson); + OUT.println("原始 Request Body: " + bodyJson); + OUT.println("加密 Request Body: " + cipherBody); + + HttpRequest request = HttpRequest.newBuilder(requestUri) + .timeout(Duration.ofSeconds(10)) + .header(ZT_APP_ID, APP_ID) + .header(ZT_TIMESTAMP, TIMESTAMP) + .header(ZT_NONCE, nonce) + .header(ZT_SIGNATURE, signature) + .header(ZT_AUTH_TOKEN, "a5d7cf609c0b47038ea405c660726ee9") + .header(CONTENT_TYPE, "application/json") + .POST(HttpRequest.BodyPublishers.ofString(cipherBody, StandardCharsets.UTF_8)) + .build(); + + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + printResponse(response); + } + + private static String encryptPayload(String plaintext) { + try { + return CryptoSignatureUtils.encrypt(plaintext, APP_SECRET, ENCRYPTION_TYPE); + } catch (Exception ex) { + throw new IllegalStateException("Failed to encrypt request body", ex); + } + } + + private static void printResponse(HttpResponse response) { + OUT.println("HTTP Status: " + response.statusCode()); + String cipherText = response.body(); + OUT.println("加密 Response: " + cipherText); + String plain = tryDecrypt(cipherText); + OUT.println("原始 Response: " + normalizePotentialMojibake(plain)); + } + + private static String randomNonce() { + return UUID.randomUUID().toString().replace("-", ""); + } + + private static URI buildUri(String baseUrl, Map queryParams) { + if (queryParams == null || queryParams.isEmpty()) { + return URI.create(baseUrl); + } + StringBuilder builder = new StringBuilder(baseUrl); + builder.append(baseUrl.contains("?") ? '&' : '?'); + boolean first = true; + for (Map.Entry entry : queryParams.entrySet()) { + if (!first) { + builder.append('&'); + } + first = false; + builder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)); + builder.append('='); + builder.append(URLEncoder.encode(String.valueOf(entry.getValue()), StandardCharsets.UTF_8)); + } + return URI.create(builder.toString()); + } + + private static String generateSignature(Map queryParams, Map bodyParams) { + TreeMap sorted = new TreeMap<>(); + if (queryParams != null) { + queryParams.forEach((key, value) -> sorted.put(key, normalizeValue(value))); + } + if (bodyParams != null) { + bodyParams.forEach((key, value) -> sorted.put(key, normalizeValue(value))); + } + sorted.put(ZT_APP_ID, APP_ID); + sorted.put(ZT_TIMESTAMP, TIMESTAMP); + StringBuilder canonical = new StringBuilder(); + sorted.forEach((key, value) -> { + if (value == null) { + return; + } + if (canonical.length() > 0) { + canonical.append('&'); + } + canonical.append(key).append('=').append(value); + }); + return md5Hex(canonical.toString()); + } + + private static Object normalizeValue(Object value) { + if (value == null) { + return null; + } + if (value instanceof Map || value instanceof Iterable) { + try { + return OBJECT_MAPPER.writeValueAsString(value); + } catch (JsonProcessingException ignored) { + return value.toString(); + } + } + return value; + } + + private static String md5Hex(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("MD5"); + byte[] bytes = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + String segment = Integer.toHexString(b & 0xFF); + if (segment.length() == 1) { + hex.append('0'); + } + hex.append(segment); + } + return hex.toString(); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("MD5 algorithm not available", ex); + } + } + + private static String tryDecrypt(String cipherText) { + if (cipherText == null || cipherText.isBlank()) { + return cipherText; + } + try { + // Databus 会在凭证开启加密时返回密文,这里做一次解密展示真实响应。 + return CryptoSignatureUtils.decrypt(cipherText, APP_SECRET, ENCRYPTION_TYPE); + } catch (Exception ex) { + return " " + ex.getMessage(); + } + } + + // 解决控制台打印 乱码问题 + private static String normalizePotentialMojibake(String value) { + if (value == null || value.isEmpty()) { + return value; + } + long suspectCount = value.chars().filter(ch -> ch >= 0x80 && ch <= 0xFF).count(); + long highCount = value.chars().filter(ch -> ch > 0xFF).count(); + if (suspectCount > 0 && highCount == 0) { + try { + byte[] decoded = value.getBytes(StandardCharsets.ISO_8859_1); + String converted = new String(decoded, StandardCharsets.UTF_8); + if (converted.chars().anyMatch(ch -> ch > 0xFF)) { + return converted; + } + } catch (Exception ignored) { + return value; + } + } + return value; + } + + /** + * 输出流编码与当前控制台保持一致,避免中文字符再次出现编码差异。 + */ + private static PrintStream buildConsolePrintStream() { + try { + String consoleEncoding = System.getProperty("sun.stdout.encoding"); + if (consoleEncoding != null && !consoleEncoding.isBlank()) { + return new PrintStream(System.out, true, Charset.forName(consoleEncoding)); + } + return new PrintStream(System.out, true, Charset.defaultCharset()); + } catch (Exception ignored) { + return System.out; + } + } + +} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java index f19f19d8..62e34af3 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java @@ -1,24 +1,34 @@ package com.zt.plat.module.databus.framework.integration.gateway.security; +import cn.hutool.crypto.SecureUtil; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +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.web.core.util.WebFrameworkUtils; 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.ApiAnonymousUserService; import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; -import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; import java.time.Duration; import java.util.Collections; import java.util.Optional; import java.util.UUID; +import static com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -27,13 +37,21 @@ import static org.mockito.Mockito.when; class GatewaySecurityFilterTest { + @AfterEach + void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + @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()); + ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); + when(anonymousUserService.issueAccessToken(any())).thenReturn(Optional.empty()); + when(anonymousUserService.issueAccessToken(any())).thenReturn(Optional.empty()); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper()); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); request.setRemoteAddr("127.0.0.1"); @@ -54,7 +72,9 @@ class GatewaySecurityFilterTest { properties.getSecurity().setEnabled(false); StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); - GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, new ObjectMapper()); + ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); + when(anonymousUserService.issueAccessToken(any())).thenReturn(Optional.empty()); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper()); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); request.setRemoteAddr("10.0.0.1"); @@ -76,6 +96,8 @@ class GatewaySecurityFilterTest { when(valueOperations.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(Boolean.TRUE); ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); + when(anonymousUserService.issueAccessToken(any())).thenReturn(Optional.empty()); ApiClientCredentialDO credential = new ApiClientCredentialDO(); credential.setAppId("demo-app"); credential.setSignatureType(null); @@ -86,17 +108,17 @@ class GatewaySecurityFilterTest { properties.getSecurity().setRequireBodyEncryption(false); properties.getSecurity().setEncryptResponse(false); - GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, new ObjectMapper()); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, 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); + String signature = signatureForApp("demo-app"); + request.addHeader(APP_ID_HEADER, "demo-app"); + request.addHeader(TIMESTAMP_HEADER, String.valueOf(timestamp)); + request.addHeader(NONCE_HEADER, nonce); + request.addHeader(SIGNATURE_HEADER, signature); MockHttpServletResponse response = new MockHttpServletResponse(); MockFilterChain chain = new MockFilterChain(); @@ -119,22 +141,23 @@ class GatewaySecurityFilterTest { when(valueOperations.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(Boolean.TRUE); ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.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()); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, 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"); + request.addHeader(APP_ID_HEADER, "demo-app"); + request.addHeader(TIMESTAMP_HEADER, String.valueOf(timestamp)); + request.addHeader(NONCE_HEADER, nonce); + request.addHeader(SIGNATURE_HEADER, "invalid-signature"); MockHttpServletResponse response = new MockHttpServletResponse(); filter.doFilter(request, response, new MockFilterChain()); @@ -150,10 +173,65 @@ class GatewaySecurityFilterTest { assertThat(node.get("message").asText()).isEqualTo("签名校验失败"); } + @Test + void shouldAuthenticateWithAnonymousUserWhenConfigured() throws Exception { + ApiGatewayProperties properties = createProperties(); + StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); + ValueOperations valueOperations = mock(ValueOperations.class); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(Boolean.TRUE); + + ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); + ApiClientCredentialDO credential = new ApiClientCredentialDO(); + credential.setAppId("demo-app"); + credential.setSignatureType(null); + credential.setEncryptionKey(null); + credential.setEncryptionType(null); + credential.setAllowAnonymous(Boolean.TRUE); + credential.setAnonymousUserId(99L); + when(credentialService.findActiveCredential("demo-app")).thenReturn(Optional.of(credential)); + + LoginUser loginUser = new LoginUser(); + loginUser.setId(999L); + loginUser.setUserType(2); + loginUser.setTenantId(123L); + ApiAnonymousUserService.AnonymousUserDetails details = new ApiAnonymousUserService.AnonymousUserDetails(99L, "匿名", loginUser); + when(anonymousUserService.find(99L)).thenReturn(Optional.of(details)); + when(anonymousUserService.issueAccessToken(details)).thenReturn(Optional.of("mock-token")); + + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, 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(APP_ID_HEADER, "demo-app"); + request.addHeader(TIMESTAMP_HEADER, String.valueOf(timestamp)); + request.addHeader(NONCE_HEADER, nonce); + request.addHeader(SIGNATURE_HEADER, signatureForApp("demo-app")); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(); + + filter.doFilter(request, response, chain); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(SecurityFrameworkUtils.getLoginUser()).isNotNull(); + assertThat(SecurityFrameworkUtils.getLoginUser().getId()).isEqualTo(999L); + assertThat(((HttpServletRequest) chain.getRequest()).getHeader(WebFrameworkUtils.HEADER_TENANT_ID)).isEqualTo("123"); + assertThat(((HttpServletRequest) chain.getRequest()).getHeader(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN)).isEqualTo("mock-token"); + assertThat(((HttpServletRequest) chain.getRequest()).getHeader(HttpHeaders.AUTHORIZATION)).isEqualTo("Bearer mock-token"); + } + private ApiGatewayProperties createProperties() { ApiGatewayProperties properties = new ApiGatewayProperties(); properties.setBasePath("/admin-api/databus/api/portal"); properties.setAllowedIps(Collections.singletonList("127.0.0.1")); return properties; } + + private String signatureForApp(String appId) { + return SecureUtil.md5("appId=" + appId); + } } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/user/dto/AdminUserRespDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/user/dto/AdminUserRespDTO.java index d726751b..497f40d5 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/user/dto/AdminUserRespDTO.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/user/dto/AdminUserRespDTO.java @@ -17,6 +17,9 @@ public class AdminUserRespDTO implements VO { @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王") private String nickname; + @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long tenantId; + @Schema(description = "帐号状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; // 参见 CommonStatusEnum 枚举 diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataScopeEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataScopeEnum.java index 0cd862c8..b0edcfd0 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataScopeEnum.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataScopeEnum.java @@ -22,8 +22,9 @@ public enum DataScopeEnum implements ArrayValuable { DEPT_CUSTOM(2), // 指定部门数据权限 DEPT_ONLY(3), // 部门数据权限 DEPT_AND_CHILD(4), // 部门及以下数据权限 + SELF(5), // 仅本人数据权限 - SELF(5); // 仅本人数据权限 + COMPANY_AND_DEPT(6); // 公司及所属部门数据权限 /** * 范围 diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/user/AdminUserApiImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/user/AdminUserApiImpl.java index 07fa5d35..a80db06e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/user/AdminUserApiImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/user/AdminUserApiImpl.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjUtil; import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.framework.datapermission.core.util.DataPermissionUtils; +import com.zt.plat.framework.tenant.core.aop.TenantIgnore; import com.zt.plat.module.system.api.user.dto.AdminUserRespDTO; import com.zt.plat.module.system.api.user.dto.AdminUserSaveReqDTO; import com.zt.plat.module.system.api.user.dto.AdminUserUpdatePasswordReqDTO; @@ -70,6 +71,7 @@ public class AdminUserApiImpl implements AdminUserApi { } @Override + @TenantIgnore public CommonResult getUser(Long id) { AdminUserDO user = userService.getUser(id); return success(BeanUtils.toBean(user, AdminUserRespDTO.class)); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthRegisterReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthRegisterReqVO.java index 42cc866c..1cbd8cce 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthRegisterReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthRegisterReqVO.java @@ -1,6 +1,7 @@ package com.zt.plat.module.system.controller.admin.auth.vo; +import com.zt.plat.framework.common.validation.Password; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; @@ -26,6 +27,7 @@ public class AuthRegisterReqVO extends CaptchaVerificationReqVO { @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") @NotEmpty(message = "密码不能为空") - @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + @Length(min = 6, max = 20, message = "密码长度为 6-20 位") + @Password private String password; } \ No newline at end of file diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthResetPasswordReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthResetPasswordReqVO.java index 37b549e7..a5781c93 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthResetPasswordReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthResetPasswordReqVO.java @@ -1,6 +1,7 @@ package com.zt.plat.module.system.controller.admin.auth.vo; import com.zt.plat.framework.common.validation.Mobile; +import com.zt.plat.framework.common.validation.Password; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; @@ -18,7 +19,8 @@ public class AuthResetPasswordReqVO { @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234") @NotEmpty(message = "密码不能为空") - @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + @Length(min = 6, max = 20, message = "密码长度为 6-20 位") + @Password private String password; @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13312341234") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthVerifyPasswordReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthVerifyPasswordReqVO.java index 59aad245..7e6936b3 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthVerifyPasswordReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthVerifyPasswordReqVO.java @@ -1,7 +1,6 @@ package com.zt.plat.module.system.controller.admin.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -13,9 +12,8 @@ import org.hibernate.validator.constraints.Length; @AllArgsConstructor public class AuthVerifyPasswordReqVO { - @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") - @NotEmpty(message = "密码不能为空") - @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + @Schema(description = "密码", example = "buzhidao") + @Length(max = 16, message = "密码长度不能超过 16 位") private String password; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java index 176097fb..50371ce2 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java @@ -136,6 +136,15 @@ public class DeptController { return success(BeanUtils.toBean(list, DeptRespVO.class)); } + @GetMapping("/search") + @Operation(summary = "根据关键字搜索部门树", description = "返回匹配节点及其上级节点,便于前端展示搜索结果") + @Parameter(name = "name", description = "部门名称关键字", required = true, example = "研发") + @PreAuthorize("@ss.hasPermission('system:dept:query')") + public CommonResult> searchDeptTree(@RequestParam("name") String name) { + List list = deptService.searchDeptTree(name); + return success(BeanUtils.toBean(list, DeptRespVO.class)); + } + @GetMapping("/get") @Operation(summary = "获得部门信息") @Parameter(name = "id", description = "编号", required = true, example = "1024") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java index b366f910..7d5a45ca 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java @@ -10,6 +10,9 @@ public class DeptListReqVO { @Schema(description = "部门名称,模糊匹配", example = "芋道") private String name; + @Schema(description = "部门编码,精确匹配", example = "ZT001") + private String code; + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/tenant/vo/tenant/TenantSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/tenant/vo/tenant/TenantSaveReqVO.java index f6ca830e..36c8d0b3 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/tenant/vo/tenant/TenantSaveReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/tenant/vo/tenant/TenantSaveReqVO.java @@ -2,14 +2,14 @@ package com.zt.plat.module.system.controller.admin.tenant.vo.tenant; import cn.hutool.core.util.ObjectUtil; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.zt.plat.framework.common.validation.Password; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import org.hibernate.validator.constraints.Length; - import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import lombok.Data; +import org.hibernate.validator.constraints.Length; import java.time.LocalDateTime; @Schema(description = "管理后台 - 租户创建/修改 Request VO") @@ -57,7 +57,8 @@ public class TenantSaveReqVO { private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") - @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + @Length(min = 6, max = 20, message = "密码长度为 6-20 位") + @Password private String password; @AssertTrue(message = "用户账号、密码不能为空") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/UserController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/UserController.java index 69863ee6..e2ebe503 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/UserController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/UserController.java @@ -11,7 +11,6 @@ import com.zt.plat.module.system.controller.admin.user.vo.user.*; import com.zt.plat.module.system.convert.user.UserConvert; import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO; import com.zt.plat.module.system.enums.common.SexEnum; -import com.zt.plat.module.system.service.dept.DeptService; import com.zt.plat.module.system.service.user.AdminUserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -38,8 +37,6 @@ public class UserController { @Resource private AdminUserService userService; - @Resource - private DeptService deptService; @PostMapping("/create") @Operation(summary = "新增用户") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java index 2efcafac..779d7002 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java @@ -1,11 +1,11 @@ package com.zt.plat.module.system.controller.admin.user.vo.profile; +import com.zt.plat.framework.common.validation.Password; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; import lombok.Data; import org.hibernate.validator.constraints.Length; -import jakarta.validation.constraints.NotEmpty; - @Schema(description = "管理后台 - 用户个人中心更新密码 Request VO") @Data public class UserProfileUpdatePasswordReqVO { @@ -17,7 +17,8 @@ public class UserProfileUpdatePasswordReqVO { @Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "654321") @NotEmpty(message = "新密码不能为空") - @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + @Length(min = 6, max = 20, message = "密码长度为 6-20 位") + @Password private String newPassword; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserRespVO.java index 72fa8d46..62fecf8e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserRespVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserRespVO.java @@ -34,9 +34,9 @@ public class UserRespVO{ @Schema(description = "部门ID列表", example = "我是一个部门Id列表") private List deptIds; -// @Schema(description = "部门名称", example = "IT 部") -// @ExcelProperty("部门名称") -// private String deptName; + + @Schema(description = "部门名称", example = "总部研发部、平台组") + private String deptNames; @Schema(description = "岗位编号数组", example = "1") private Set postIds; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java index 43865014..fb416258 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java @@ -1,12 +1,13 @@ package com.zt.plat.module.system.controller.admin.user.vo.user; import cn.hutool.core.util.ObjectUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.mzt.logapi.starter.annotation.DiffLogField; import com.zt.plat.framework.common.validation.Mobile; +import com.zt.plat.framework.common.validation.Password; import com.zt.plat.module.system.framework.operatelog.core.DeptParseFunction; import com.zt.plat.module.system.framework.operatelog.core.PostParseFunction; import com.zt.plat.module.system.framework.operatelog.core.SexParseFunction; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.mzt.logapi.starter.annotation.DiffLogField; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; import lombok.Data; @@ -76,7 +77,8 @@ public class UserSaveReqVO { // ========== 仅【创建】时,需要传递的字段 ========== @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") - @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + @Length(min = 6, max = 20, message = "密码长度为 6-20 位") + @Password private String password; @AssertTrue(message = "密码不能为空") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSelectorPageReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSelectorPageReqVO.java new file mode 100644 index 00000000..8aa52fa2 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSelectorPageReqVO.java @@ -0,0 +1,22 @@ +package com.zt.plat.module.system.controller.admin.user.vo.user; + +import com.zt.plat.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 用户选择器分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class UserSelectorPageReqVO extends PageParam { + + @Schema(description = "关键字(支持姓名/账号/手机号模糊匹配)", example = "张三") + private String keyword; + + @Schema(description = "部门编号,同时筛选子部门", example = "1024") + private Long deptId; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "0") + private Integer status; + +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSelectorRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSelectorRespVO.java new file mode 100644 index 00000000..defe32ff --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSelectorRespVO.java @@ -0,0 +1,27 @@ +package com.zt.plat.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 用户选择器 Response VO") +@Data +public class UserSelectorRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王小明") + private String nickname; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "wangxm") + private String username; + + @Schema(description = "手机号码", example = "13800000000") + private String mobile; + + @Schema(description = "帐号状态,参见 CommonStatusEnum 枚举类", example = "0") + private Integer status; + + @Schema(description = "所属部门名称(多个部门以、分隔)", example = "总部研发部、平台组") + private String deptNames; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java index 554d9bf9..20bee5cc 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java @@ -1,11 +1,11 @@ package com.zt.plat.module.system.controller.admin.user.vo.user; +import com.zt.plat.framework.common.validation.Password; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import org.hibernate.validator.constraints.Length; - import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; @Schema(description = "管理后台 - 用户更新密码 Request VO") @Data @@ -17,7 +17,8 @@ public class UserUpdatePasswordReqVO { @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") @NotEmpty(message = "密码不能为空") - @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + @Length(min = 6, max = 20, message = "密码长度为 6-20 位") + @Password private String password; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/user/UserConvert.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/user/UserConvert.java index 9e3a5288..414b695a 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/user/UserConvert.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/user/UserConvert.java @@ -30,7 +30,11 @@ public interface UserConvert { } default UserRespVO convert(AdminUserDO user) { - return BeanUtils.toBean(user, UserRespVO.class); + UserRespVO vo = BeanUtils.toBean(user, UserRespVO.class); + if (user.getDeptIds() != null) { + vo.setDeptIds(CollectionUtils.convertList(user.getDeptIds(), Long::longValue)); + } + return vo; } default List convertSimpleList(List list) { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/user/AdminUserDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/user/AdminUserDO.java index e553c3f2..1b1a63bd 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/user/AdminUserDO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/user/AdminUserDO.java @@ -60,6 +60,9 @@ public class AdminUserDO extends TenantBaseDO { @TableField(exist = false, typeHandler = JacksonTypeHandler.class ) @NotEmpty private Set deptIds; + + @TableField(exist = false) + private String deptNames; /** * 公司 ID 列表 */ diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java index 04f841a3..009fe39b 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java @@ -1,11 +1,12 @@ package com.zt.plat.module.system.dal.mysql.dept; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.zt.plat.framework.common.enums.CommonStatusEnum; import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; 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; @@ -20,6 +21,7 @@ public interface DeptMapper extends BaseMapperX { default List selectList(DeptListReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(DeptDO::getName, reqVO.getName()) + .eqIfPresent(DeptDO::getCode, reqVO.getCode()) .eqIfPresent(DeptDO::getStatus, reqVO.getStatus()) .eqIfPresent(DeptDO::getIsCompany, reqVO.getIsCompany()) ); @@ -127,4 +129,17 @@ public interface DeptMapper extends BaseMapperX { ); } + /** + * 根据部门名称模糊查询启用状态的部门列表 + * + * @param name 部门名称关键字 + * @return 部门列表 + */ + default List selectListByName(String name) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DeptDO::getName, name) + .eq(DeptDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + ); + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java index 7bc3e06d..6489d903 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java @@ -1,6 +1,10 @@ package com.zt.plat.module.system.service.auth; import cn.hutool.core.util.ObjectUtil; +import com.anji.captcha.model.common.ResponseModel; +import com.anji.captcha.model.vo.CaptchaVO; +import com.anji.captcha.service.CaptchaService; +import com.google.common.annotations.VisibleForTesting; import com.zt.plat.framework.common.enums.CommonStatusEnum; import com.zt.plat.framework.common.enums.UserTypeEnum; import com.zt.plat.framework.common.util.monitor.TracerUtils; @@ -19,17 +23,13 @@ import com.zt.plat.module.system.enums.logger.LoginLogTypeEnum; import com.zt.plat.module.system.enums.logger.LoginResultEnum; import com.zt.plat.module.system.enums.oauth2.OAuth2ClientConstants; import com.zt.plat.module.system.enums.sms.SmsSceneEnum; +import com.zt.plat.module.system.enums.user.UserSourceEnum; import com.zt.plat.module.system.service.logger.LoginLogService; import com.zt.plat.module.system.service.member.MemberService; -import com.zt.plat.module.system.service.oauth2.OAuth2TokenService; import com.zt.plat.module.system.service.oauth2.EbanOAuth2Service; +import com.zt.plat.module.system.service.oauth2.OAuth2TokenService; import com.zt.plat.module.system.service.social.SocialUserService; import com.zt.plat.module.system.service.user.AdminUserService; -import com.zt.plat.module.system.enums.user.UserSourceEnum; -import com.anji.captcha.model.common.ResponseModel; -import com.anji.captcha.model.vo.CaptchaVO; -import com.anji.captcha.service.CaptchaService; -import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; import jakarta.validation.Validator; import lombok.Setter; @@ -286,6 +286,16 @@ public class AdminAuthServiceImpl implements AdminAuthService { if (user == null) { throw exception(USER_NOT_EXISTS); } + if (isInternalUser(user)) { + return; + } + if (StringUtils.isBlank(password)) { + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + int length = password.length(); + if (length < 4 || length > 16) { + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } if (!userService.isPasswordMatch(password, user.getPassword())) { throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java index 2181426e..c97510df 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java @@ -128,6 +128,15 @@ public interface DeptService { Set getCompanyDeptInfoListByUserId(Long userId); + /** + * 计算公司及其直属部门数据权限范围 + * 在给定部门集合的场景下,按所属公司维度汇总公司本级及其直属部门(不含其他子公司) + * + * @param deptIds 部门编号集合 + * @return 公司及其直属部门编号集合 + */ + Set computeCompanyScopeDeptIds(Set deptIds); + /** * 获取当前用户可访问的顶级部门列表 * 用于懒加载,返回当前用户所属部门的最顶层祖先部门 @@ -157,4 +166,12 @@ public interface DeptService { * 按照新的编码规则初始化全部部门编码 */ void initializeDeptCodes(); + + /** + * 根据关键字搜索部门树,包含匹配节点及其上级节点 + * + * @param keyword 关键字 + * @return 部门列表 + */ + List searchDeptTree(String keyword); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java index 918fc182..b3adfa26 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java @@ -400,19 +400,11 @@ public class DeptServiceImpl implements DeptService { return Collections.emptyList(); } Set companyIds = new HashSet<>(); + Map deptCache = new HashMap<>(); for (Long deptId : deptIds) { - DeptDO dept = getDept(deptId); - while (dept != null) { - // 如果当前部门是公司,加入结果并结束本次递归 - if (Boolean.TRUE.equals(dept.getIsCompany())) { - companyIds.add(dept.getId()); - break; - } - // 到达根节点或无上级,结束递归 - if (dept.getParentId() == null || DeptDO.PARENT_ID_ROOT.equals(dept.getParentId())) { - break; - } - dept = getDept(dept.getParentId()); + Long companyId = resolveNearestCompanyId(deptId, deptCache); + if (companyId != null) { + companyIds.add(companyId); } } return getDeptList(companyIds); @@ -440,43 +432,15 @@ public class DeptServiceImpl implements DeptService { // 如果指定了公司ID,则进一步过滤属于该公司的部门 if (companyId != null) { + Map deptCache = new HashMap<>(); return deptList.stream() - .filter(dept -> isUnderCompany(dept, companyId)) + .filter(dept -> Objects.equals(resolveNearestCompanyId(dept.getId(), deptCache), companyId)) .collect(Collectors.toList()); } return deptList; } - /** - * 判断部门是否属于指定公司 - * - * @param dept 部门 - * @param companyId 公司ID - * @return 是否属于指定公司 - */ - private boolean isUnderCompany(DeptDO dept, Long companyId) { - if (dept == null || companyId == null) { - return false; - } - - // 如果部门本身就是指定的公司 - if (dept.getId().equals(companyId) && Boolean.TRUE.equals(dept.getIsCompany())) { - return true; - } - - // 向上递归查找,看是否有祖先部门是指定的公司 - DeptDO current = dept; - while (current != null && current.getParentId() != null && !DeptDO.PARENT_ID_ROOT.equals(current.getParentId())) { - current = getDept(current.getParentId()); - if (current != null && current.getId().equals(companyId) && Boolean.TRUE.equals(current.getIsCompany())) { - return true; - } - } - - return false; - } - /** * 根据用户ID查询其归属公司及直属部门关系列表(不递归下级公司) */ @@ -493,18 +457,13 @@ public class DeptServiceImpl implements DeptService { // 查询所有部门信息 Map deptMap = getDeptList(deptIds).stream() .collect(Collectors.toMap(DeptDO::getId, d -> d)); + Map deptCache = new HashMap<>(deptMap); Set result = new HashSet<>(); for (Long deptId : deptIds) { - DeptDO dept = deptMap.get(deptId); + DeptDO dept = loadDept(deptId, deptCache); if (dept == null) continue; - // 向上查找公司,如果到达顶层(parentId为PARENT_ID_ROOT)还没找到公司,则用顶层部门作为公司 - DeptDO company = dept; - while (company != null && !Boolean.TRUE.equals(company.getIsCompany())) { - if (company.getParentId() == null || DeptDO.PARENT_ID_ROOT.equals(company.getParentId())) { - break; - } - company = getDept(company.getParentId()); - } + Long companyId = resolveNearestCompanyId(deptId, deptCache); + DeptDO company = companyId != null ? loadDept(companyId, deptCache) : findTopLevelAncestor(dept); if (company == null) continue; CompanyDeptInfo info = new CompanyDeptInfo(); info.setCompanyId(company.getId()); @@ -518,6 +477,26 @@ public class DeptServiceImpl implements DeptService { return result; } + @Override + public Set computeCompanyScopeDeptIds(Set deptIds) { + if (CollUtil.isEmpty(deptIds)) { + return Collections.emptySet(); + } + Map deptCache = new HashMap<>(); + Map> companyDeptCache = new HashMap<>(); + Set result = new HashSet<>(); + for (Long deptId : deptIds) { + Long companyId = resolveNearestCompanyId(deptId, deptCache); + if (companyId == null) { + continue; + } + Set scopedDeptIds = companyDeptCache.computeIfAbsent(companyId, + id -> resolveCompanyDeptIds(id, deptCache)); + result.addAll(scopedDeptIds); + } + return result; + } + @Override public List getTopLevelDeptList() { // 获取当前用户所属的部门列表 @@ -614,4 +593,93 @@ public class DeptServiceImpl implements DeptService { } } + @Override + public List searchDeptTree(String keyword) { + if (StrUtil.isBlank(keyword)) { + return Collections.emptyList(); + } + List matchedDepts = deptMapper.selectListByName(keyword.trim()); + if (CollUtil.isEmpty(matchedDepts)) { + return Collections.emptyList(); + } + + Map cache = new HashMap<>(); + LinkedHashMap resultMap = new LinkedHashMap<>(); + for (DeptDO dept : matchedDepts) { + appendDeptWithAncestors(dept, resultMap, cache); + } + return new ArrayList<>(resultMap.values()); + } + + private void appendDeptWithAncestors(DeptDO dept, Map resultMap, Map cache) { + DeptDO current = dept; + while (current != null) { + if (!resultMap.containsKey(current.getId())) { + resultMap.put(current.getId(), current); + } + Long parentId = current.getParentId(); + if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) { + break; + } + if (Objects.equals(parentId, current.getId())) { + break; + } + current = cache.computeIfAbsent(parentId, deptMapper::selectById); + } + } + + private Set resolveCompanyDeptIds(Long companyId, Map deptCache) { + DeptDO company = loadDept(companyId, deptCache); + if (company == null) { + return Collections.emptySet(); + } + Set deptIds = new HashSet<>(); + deptIds.add(company.getId()); + List descendants = getChildDeptList(companyId); + if (CollUtil.isEmpty(descendants)) { + return deptIds; + } + descendants.forEach(dept -> deptCache.putIfAbsent(dept.getId(), dept)); + for (DeptDO dept : descendants) { + if (Boolean.TRUE.equals(dept.getIsCompany())) { + continue; + } + Long anchorCompanyId = resolveNearestCompanyId(dept.getId(), deptCache); + if (Objects.equals(anchorCompanyId, companyId)) { + deptIds.add(dept.getId()); + } + } + return deptIds; + } + + private Long resolveNearestCompanyId(Long deptId, Map deptCache) { + DeptDO current = loadDept(deptId, deptCache); + while (current != null) { + if (Boolean.TRUE.equals(current.getIsCompany())) { + return current.getId(); + } + Long parentId = current.getParentId(); + if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) { + return null; + } + current = loadDept(parentId, deptCache); + } + return null; + } + + private DeptDO loadDept(Long deptId, Map deptCache) { + if (deptId == null) { + return null; + } + DeptDO cached = deptCache.get(deptId); + if (cached != null) { + return cached; + } + DeptDO dept = getDept(deptId); + if (dept != null) { + deptCache.put(deptId, dept); + } + return dept; + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2Service.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2Service.java index 9a278c78..d1900b36 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2Service.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2Service.java @@ -1,7 +1,8 @@ package com.zt.plat.module.system.service.oauth2; -import com.zt.plat.module.system.controller.admin.auth.vo.AuthOAuth2CallbackReqVO; import com.zt.plat.module.system.controller.admin.auth.vo.AuthLoginRespVO; +import com.zt.plat.module.system.controller.admin.auth.vo.AuthOAuth2CallbackReqVO; +import lombok.Data; /** * E办OAuth2服务接口 @@ -30,6 +31,7 @@ public interface EbanOAuth2Service { /** * E办用户信息 */ + @Data class EbanUserInfo { private String username; private String realName; @@ -37,47 +39,12 @@ public interface EbanOAuth2Service { private String mobile; private String deptName; private String uid; - private String displayName; + private String displayName; + private String loginName; private String rawUserInfoJson; private EbanOAuth2ServiceImpl.EbanTokenInfo tokenInfo; // 添加Token信息 // 构造函数 public EbanUserInfo() {} - - public EbanUserInfo(String username, String realName, String email, String mobile, String deptName) { - this.username = username; - this.realName = realName; - this.email = email; - this.mobile = mobile; - this.deptName = deptName; - } - - // getter和setter方法 - public String getUsername() { return username; } - public void setUsername(String username) { this.username = username; } - - public String getRealName() { return realName; } - public void setRealName(String realName) { this.realName = realName; } - - public String getEmail() { return email; } - public void setEmail(String email) { this.email = email; } - - public String getMobile() { return mobile; } - public void setMobile(String mobile) { this.mobile = mobile; } - - public String getDeptName() { return deptName; } - public void setDeptName(String deptName) { this.deptName = deptName; } - - public String getUid() { return uid; } - public void setUid(String uid) { this.uid = uid; } - - public String getDisplayName() { return displayName; } - public void setDisplayName(String displayName) { this.displayName = displayName; } - - public String getRawUserInfoJson() { return rawUserInfoJson; } - public void setRawUserInfoJson(String rawUserInfoJson) { this.rawUserInfoJson = rawUserInfoJson; } - - public EbanOAuth2ServiceImpl.EbanTokenInfo getTokenInfo() { return tokenInfo; } - public void setTokenInfo(EbanOAuth2ServiceImpl.EbanTokenInfo tokenInfo) { this.tokenInfo = tokenInfo; } } } \ No newline at end of file diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2ServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2ServiceImpl.java index 41e3c8c9..40cea9c9 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2ServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanOAuth2ServiceImpl.java @@ -14,7 +14,6 @@ import com.zt.plat.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO; import com.zt.plat.module.system.enums.logger.LoginLogTypeEnum; import com.zt.plat.module.system.enums.logger.LoginResultEnum; -import com.zt.plat.module.system.enums.oauth2.OAuth2ClientConstants; import com.zt.plat.module.system.service.logger.LoginLogService; import com.zt.plat.module.system.service.user.AdminUserService; import jakarta.annotation.Resource; @@ -26,7 +25,6 @@ import java.util.HashMap; import java.util.Map; import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.zt.plat.framework.common.util.servlet.ServletUtils.getClientIP; import static com.zt.plat.module.system.enums.ErrorCodeConstants.*; /** @@ -46,9 +44,6 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { @Resource private LoginLogService loginLogService; - @Resource - private OAuth2TokenService oauth2TokenService; - @Resource private EbanTokenService ebanTokenService; @@ -77,16 +72,16 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID); } - String displayName = StrUtil.trim(StrUtil.blankToDefault(userInfo.getDisplayName(), userInfo.getUsername())); - if (StrUtil.isBlank(displayName)) { - log.error("E办OAuth2用户信息缺少displayName与loginName,无法匹配账号: {}", JSONUtil.toJsonStr(userInfo)); + String username = StrUtil.trim(StrUtil.blankToDefault(userInfo.getLoginName(), userInfo.getUsername())); + if (StrUtil.isBlank(username)) { + log.error("E办OAuth2用户信息缺少 username 与 loginName,无法匹配账号: {}", JSONUtil.toJsonStr(userInfo)); throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID); } - AdminUserDO user = userService.getUserByUsername(displayName); + AdminUserDO user = userService.getUserByUsername(username); if (user == null) { - createLoginLog(null, displayName, LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.BAD_CREDENTIALS); - log.warn("E办OAuth2用户displayName未在系统中找到对应账号: {}", displayName); + createLoginLog(null, username, LoginLogTypeEnum.LOGIN_SOCIAL, LoginResultEnum.BAD_CREDENTIALS); + log.warn("E办OAuth2用户displayName未在系统中找到对应账号: {}", username); throw exception(AUTH_LOGIN_EBAN_USER_NOT_SYNC); } @@ -95,27 +90,24 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { throw exception(AUTH_LOGIN_USER_DISABLED); } - try { - EbanTokenInfo tokenInfo = userInfo.getTokenInfo(); - if (tokenInfo != null && StrUtil.isNotBlank(tokenInfo.getAccessToken())) { - String userInfoJson = StrUtil.blankToDefault(userInfo.getRawUserInfoJson(), buildBasicUserInfoJson(userInfo)); - Long tenantId = user.getTenantId() != null ? user.getTenantId() : 0L; - ebanTokenService.createEbanToken( - user.getId(), - tenantId, - tokenInfo.getAccessToken(), - tokenInfo.getRefreshToken(), - tokenInfo.getExpiresIn(), - userInfo.getUid(), - userInfoJson - ); - log.info("成功保存E办token,userId={}, uid={}, displayName={}", user.getId(), userInfo.getUid(), displayName); - } - } catch (Exception e) { - log.error("保存E办token失败,userId={}, uid={}, displayName={}", user.getId(), userInfo.getUid(), displayName, e); + EbanTokenInfo tokenInfo = userInfo.getTokenInfo(); + if (tokenInfo == null || StrUtil.isBlank(tokenInfo.getAccessToken())) { + log.error("E办OAuth2回调缺少有效的token信息,uid={}, username={}", userInfo.getUid(), username); + throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID); } - return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); + Long tenantId = user.getTenantId() != null ? user.getTenantId() : 0L; + OAuth2AccessTokenDO ebanAccessTokenDO = ebanTokenService.createEbanToken( + user.getId(), + tenantId, + tokenInfo.getAccessToken(), + tokenInfo.getRefreshToken(), + tokenInfo.getExpiresIn(), + userInfo + ); + log.info("成功保存E办token,userId={}, uid={}, username={}", user.getId(), userInfo.getUid(), username); + + return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL, ebanAccessTokenDO); } catch (ServiceException e) { throw e; @@ -139,18 +131,6 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { return userInfo; } - private String buildBasicUserInfoJson(EbanUserInfo userInfo) { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("uid", userInfo.getUid()); - jsonObject.put("displayName", userInfo.getDisplayName()); - jsonObject.put("loginName", userInfo.getUsername()); - jsonObject.put("realName", userInfo.getRealName()); - jsonObject.put("email", userInfo.getEmail()); - jsonObject.put("mobile", userInfo.getMobile()); - jsonObject.put("deptName", userInfo.getDeptName()); - return jsonObject.toString(); - } - /** * E办Token信息 */ @@ -340,18 +320,15 @@ public class EbanOAuth2ServiceImpl implements EbanOAuth2Service { /** * 登录成功后创建Token */ - private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { + private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType, + OAuth2AccessTokenDO accessTokenDO) { // 插入登陆日志 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); - - // 创建访问令牌 - OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( - userId, - com.zt.plat.framework.common.enums.UserTypeEnum.ADMIN.getValue(), - OAuth2ClientConstants.CLIENT_ID_DEFAULT, - null - ); - + + if (accessTokenDO == null) { + throw exception(AUTH_LOGIN_EBAN_TOKEN_INVALID); + } + // 构建返回结果 return AuthConvert.INSTANCE.convert(accessTokenDO); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanTokenService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanTokenService.java index cdb3f429..85bab9f4 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanTokenService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanTokenService.java @@ -17,12 +17,11 @@ public interface EbanTokenService { * @param accessToken E办访问令牌 * @param refreshToken E办刷新令牌 * @param expiresIn 过期时间(秒) - * @param uid E办用户唯一标识 - * @param userInfo E办用户信息(JSON格式) + * @param ebanUserInfo e办用户信息 * @return OAuth2AccessTokenDO */ OAuth2AccessTokenDO createEbanToken(Long userId, Long tenantId, String accessToken, String refreshToken, - Integer expiresIn, String uid, String userInfo); + Integer expiresIn, EbanOAuth2Service.EbanUserInfo ebanUserInfo); /** * 根据用户ID获取E办Token @@ -54,7 +53,7 @@ public interface EbanTokenService { * @return 是否有效 */ boolean isEbanTokenValid(Long userId); - + /** * 根据access_token获取E办Token信息 * diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanTokenServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanTokenServiceImpl.java index cd9b3fc7..f2a907bc 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanTokenServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/EbanTokenServiceImpl.java @@ -53,19 +53,26 @@ public class EbanTokenServiceImpl implements EbanTokenService { @Override public OAuth2AccessTokenDO createEbanToken(Long userId, Long tenantId, String accessToken, String refreshToken, - Integer expiresIn, String uid, String userInfo) { + Integer expiresIn, EbanOAuth2Service.EbanUserInfo userInfo) { if (StrUtil.isBlank(accessToken)) { throw ServiceExceptionUtil.exception(AUTH_LOGIN_EBAN_TOKEN_INVALID); } + if (userInfo == null) { + throw ServiceExceptionUtil.exception(AUTH_LOGIN_EBAN_TOKEN_INVALID); + } LocalDateTime expiresTime = calculateExpiresTime(expiresIn); - Map userInfoMap = MapUtil.newHashMap(2, false); - if (StrUtil.isNotBlank(uid)) { - userInfoMap.put("uid", uid); + Map userInfoMap = MapUtil.newHashMap(6, false); + if (StrUtil.isNotBlank(userInfo.getLoginName())) { + userInfoMap.put("username", userInfo.getLoginName()); } - if (StrUtil.isNotBlank(userInfo)) { - userInfoMap.put("rawUserInfo", userInfo); + if (StrUtil.isNotBlank(userInfo.getDisplayName())) { + userInfoMap.put("nickname", userInfo.getDisplayName()); } + if (StrUtil.isNotBlank(userInfo.getUid())) { + userInfoMap.put("uid", userInfo.getUid()); + } + userInfoMap.put("tenantId", String.valueOf(tenantId != null ? tenantId : 0L)); OAuth2AccessTokenDO tokenDO = oauth2AccessTokenMapper.selectByUserIdAndClientId(userId, EBAN_CLIENT_ID); if (tokenDO == null) { @@ -87,7 +94,7 @@ public class EbanTokenServiceImpl implements EbanTokenService { oauth2AccessTokenMapper.updateById(tokenDO); } - log.info("保存E办Token成功,userId={}, uid={}, expiresTime={}", userId, uid, expiresTime); + log.info("保存E办Token成功,userId={}, expiresTime={}", userId, expiresTime); return tokenDO; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java index aad99000..a86899a3 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java @@ -372,7 +372,7 @@ public class PermissionServiceImpl implements PermissionService { CollUtil.addAll(result.getDeptIds(), userDeptIds.get()); continue; } - // 情况四,DEPT_DEPT_AND_CHILD + // 情况四,DEPT_AND_CHILD if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { Set deptIds = Optional.ofNullable(userDeptIds.get()).orElseGet(Collections::emptySet); for (Long userDeptId : deptIds) { @@ -382,7 +382,15 @@ public class PermissionServiceImpl implements PermissionService { CollUtil.addAll(result.getDeptIds(), deptIds); continue; } - // 情况五,SELF + // 情况五,COMPANY_AND_DEPT + if (Objects.equals(role.getDataScope(), DataScopeEnum.COMPANY_AND_DEPT.getScope())) { + // 公司及所属部门数据范围:由部门服务汇总最近所属公司及其直属部门 + Set deptIds = deptService.computeCompanyScopeDeptIds(Optional.ofNullable(userDeptIds.get()) + .orElse(Collections.emptySet())); + CollUtil.addAll(result.getDeptIds(), deptIds); + continue; + } + // 情况六,SELF if (Objects.equals(role.getDataScope(), DataScopeEnum.SELF.getScope())) { result.setSelf(true); continue; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/UserSyncServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/UserSyncServiceImpl.java index 8e8f7602..0610c086 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/UserSyncServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/UserSyncServiceImpl.java @@ -10,11 +10,11 @@ import com.zt.plat.module.system.dal.dataobject.dept.UserPostDO; import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO; import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO; import com.zt.plat.module.system.dal.mysql.dept.UserPostMapper; +import com.zt.plat.module.system.enums.user.UserSourceEnum; import com.zt.plat.module.system.service.dept.PostService; import com.zt.plat.module.system.service.user.AdminUserService; import com.zt.plat.module.system.service.userdept.UserDeptService; import com.zt.plat.module.system.util.sync.SyncVerifyUtil; -import com.zt.plat.module.system.enums.user.UserSourceEnum; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; @@ -55,7 +55,7 @@ public class UserSyncServiceImpl implements UserSyncService { Long tenantId = Optional.ofNullable(loginUser).orElse(new LoginUser()).getTenantId(); TenantContextHolder.setTenantId(tenantId); // 中铝 e 办不会设置密码,设置默认密码 - saveReqVO.setPassword("ZLEB"); + saveReqVO.setPassword("Zgty@9527"); // 设置为同步用户 saveReqVO.setUserSource(UserSourceEnum.SYNC.getSource()); Long userId = adminUserService.createUser(saveReqVO); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java index ad214105..52703ed2 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/user/AdminUserServiceImpl.java @@ -328,7 +328,9 @@ public class AdminUserServiceImpl implements AdminUserService { permissionService.getUserRoleIdListByRoleId(singleton(reqVO.getRoleId())) : null; // 分页查询 - return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()), userIds); + PageResult pageResult = userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId()), userIds); + fillUserDeptInfo(pageResult.getList()); + return pageResult; } @Override @@ -338,7 +340,15 @@ public class AdminUserServiceImpl implements AdminUserService { adminUserDO.setDeptIds(companyDeptInfoListByUserId.stream().map(CompanyDeptInfo::getDeptId).collect(Collectors.toSet())); adminUserDO.setCompanyIds(companyDeptInfoListByUserId.stream().map(CompanyDeptInfo::getCompanyId).collect(Collectors.toSet())); adminUserDO.setCompanyDeptInfos(companyDeptInfoListByUserId); - // 设置用户的部门名称集合 + String deptNames = companyDeptInfoListByUserId.stream() + .map(CompanyDeptInfo::getDeptName) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.joining("、")); + adminUserDO.setDeptNames(StrUtil.blankToDefault(deptNames, "-")); + if (CollUtil.isEmpty(adminUserDO.getDeptIds())) { + adminUserDO.setDeptIds(Collections.emptySet()); + } return adminUserDO; } @@ -416,6 +426,48 @@ public class AdminUserServiceImpl implements AdminUserService { return deptIds; } + private void fillUserDeptInfo(List users) { + if (CollUtil.isEmpty(users)) { + return; + } + Map userMap = CollectionUtils.convertMap(users, AdminUserDO::getId); + userMap.values().forEach(user -> { + user.setDeptIds(Collections.emptySet()); + user.setDeptNames("-"); + }); + + List relations = userDeptService.getValidUserDeptListByUserIds(userMap.keySet()); + if (CollUtil.isEmpty(relations)) { + return; + } + + Map> userDeptIdsMap = new HashMap<>(); + relations.forEach(relation -> + userDeptIdsMap.computeIfAbsent(relation.getUserId(), key -> new LinkedHashSet<>()) + .add(relation.getDeptId())); + + Set allDeptIds = userDeptIdsMap.values().stream() + .flatMap(Collection::stream) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map deptMap = allDeptIds.isEmpty() ? Collections.emptyMap() : deptService.getDeptMap(allDeptIds); + + userDeptIdsMap.forEach((userId, deptIds) -> { + AdminUserDO user = userMap.get(userId); + if (user == null) { + return; + } + user.setDeptIds(deptIds); + String deptNames = deptIds.stream() + .map(deptMap::get) + .filter(Objects::nonNull) + .map(DeptDO::getName) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.joining("、")); + user.setDeptNames(StrUtil.blankToDefault(deptNames, "-")); + }); + } + private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email, Set deptIds, Set postIds) { // 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确 diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImplTest.java index 9b752c5d..e073da78 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImplTest.java @@ -16,6 +16,7 @@ import com.zt.plat.module.system.enums.sms.SmsSceneEnum; import com.zt.plat.module.system.enums.social.SocialTypeEnum; import com.zt.plat.module.system.service.logger.LoginLogService; import com.zt.plat.module.system.service.member.MemberService; +import com.zt.plat.module.system.service.oauth2.EbanOAuth2Service; import com.zt.plat.module.system.service.oauth2.OAuth2TokenService; import com.zt.plat.module.system.service.social.SocialUserService; import com.zt.plat.module.system.service.user.AdminUserService; @@ -60,6 +61,8 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { private OAuth2TokenService oauth2TokenService; @MockBean private MemberService memberService; + @MockBean + private EbanOAuth2Service ebanOAuth2Service; @MockBean private Validator validator; diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/social/SocialClientServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/social/SocialClientServiceImplTest.java index 9ae6153f..3dfc1146 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/social/SocialClientServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/social/SocialClientServiceImplTest.java @@ -457,7 +457,7 @@ public class SocialClientServiceImplTest extends BaseDbUnitTest { reqVO.setName("芋"); reqVO.setSocialType(SocialTypeEnum.GITEE.getType()); reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); - reqVO.setClientId("yu"); + reqVO.setClientId("z"); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 调用 diff --git a/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql b/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql index d869fe3e..fc9bc7a0 100644 --- a/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql +++ b/zt-module-system/zt-module-system-server/src/test/resources/sql/create_tables.sql @@ -12,7 +12,9 @@ create table IF NOT EXISTS system_user_dept ( ); CREATE TABLE IF NOT EXISTS "system_dept" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(64) NOT NULL DEFAULT '', "name" varchar(30) NOT NULL DEFAULT '', + "short_name" varchar(30) DEFAULT '', "parent_id" bigint NOT NULL DEFAULT '0', "sort" int NOT NULL DEFAULT '0', "leader_user_id" bigint DEFAULT NULL,