From 444f8cd01592d1db133bbd500d6876ca9569b97a Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 19 Feb 2026 13:03:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=9B=BE=E8=B0=B1=E6=A8=A1=E5=9D=97=20P0/P1/P2/P3=20=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【P0 - 安全风险修复】 - InternalTokenInterceptor: fail-open → fail-closed - 未配置 token 时直接拒绝(401) - 仅 dev/test 环境可显式跳过校验 - KnowledgeGraphProperties: 新增 skipTokenCheck 配置项 - application-knowledgegraph.yml: 新增 skip-token-check 配置 【P1 - 文档版本控制】 - .gitignore: 移除 docs/knowledge-graph/ 忽略规则 - schema 文档现已纳入版本控制 【P2 - 代码质量改进】 - InternalTokenInterceptor: 错误响应改为 Response.error() 格式 - 新增 InternalTokenInterceptorTest.java(7 个测试用例) - fail-closed 行为验证 - token 校验逻辑验证 - 错误响应格式验证 【P3 - 文档一致性】 - README.md: 相对链接改为显式 GitHub 链接 【验证结果】 - 编译通过 - 198 个测试全部通过(0 failures) --- .gitignore | 2 - README.md | 4 +- .../exception/KnowledgeGraphErrorCode.java | 3 +- .../neo4j/KnowledgeGraphProperties.java | 18 +++ .../security/InternalTokenInterceptor.java | 74 +++++++++ .../InternalTokenWebMvcConfigurer.java | 22 +++ .../interfaces/rest/GraphSyncController.java | 11 +- .../resources/application-knowledgegraph.yml | 8 + .../InternalTokenInterceptorTest.java | 152 ++++++++++++++++++ .../datamate/main/config/SecurityConfig.java | 10 +- .../src/main/resources/application.yml | 6 - 11 files changed, 293 insertions(+), 17 deletions(-) create mode 100644 backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenInterceptor.java create mode 100644 backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenWebMvcConfigurer.java create mode 100644 backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenInterceptorTest.java diff --git a/.gitignore b/.gitignore index dcf5d0d..32778df 100644 --- a/.gitignore +++ b/.gitignore @@ -190,5 +190,3 @@ Thumbs.db # Milvus deployment/docker/milvus/volumes/ -# Local documentation -docs/knowledge-graph/ diff --git a/README.md b/README.md index b303c7e..6b79952 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,9 @@ Thank you for your interest in this project! We warmly welcome contributions fro bug reports, suggesting new features, or directly participating in code development, all forms of help make the project better. -• 📮 [GitHub Issues](../../issues): Submit bugs or feature suggestions. +• 📮 [GitHub Issues](https://github.com/ModelEngine-Group/DataMate/issues): Submit bugs or feature suggestions. -• 🔧 [GitHub Pull Requests](../../pulls): Contribute code improvements. +• 🔧 [GitHub Pull Requests](https://github.com/ModelEngine-Group/DataMate/pulls): Contribute code improvements. ## 📄 License diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/exception/KnowledgeGraphErrorCode.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/exception/KnowledgeGraphErrorCode.java index 7585062..fe4f372 100644 --- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/exception/KnowledgeGraphErrorCode.java +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/exception/KnowledgeGraphErrorCode.java @@ -22,7 +22,8 @@ public enum KnowledgeGraphErrorCode implements ErrorCode { SYNC_FAILED("knowledge_graph.0009", "数据同步失败"), EMPTY_SNAPSHOT_PURGE_BLOCKED("knowledge_graph.0010", "空快照保护:上游返回空列表,已阻止 purge 操作"), SCHEMA_INIT_FAILED("knowledge_graph.0011", "图谱 Schema 初始化失败"), - INSECURE_DEFAULT_CREDENTIALS("knowledge_graph.0012", "检测到默认凭据,生产环境禁止使用默认密码"); + INSECURE_DEFAULT_CREDENTIALS("knowledge_graph.0012", "检测到默认凭据,生产环境禁止使用默认密码"), + UNAUTHORIZED_INTERNAL_CALL("knowledge_graph.0013", "内部调用未授权:X-Internal-Token 校验失败"); private final String code; private final String message; diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/neo4j/KnowledgeGraphProperties.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/neo4j/KnowledgeGraphProperties.java index 650286b..fe0b954 100644 --- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/neo4j/KnowledgeGraphProperties.java +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/neo4j/KnowledgeGraphProperties.java @@ -25,6 +25,24 @@ public class KnowledgeGraphProperties { /** 同步相关配置 */ private Sync sync = new Sync(); + /** 安全相关配置 */ + private Security security = new Security(); + + @Data + public static class Security { + + /** 内部服务调用 Token,用于校验 sync 端点的 X-Internal-Token 请求头 */ + private String internalToken; + + /** + * 是否跳过内部 Token 校验(默认 false,即 fail-closed)。 + *

+ * 仅允许在 dev/test 环境显式设置为 true 以跳过校验。 + * 生产环境必须保持 false 并配置 {@code internal-token}。 + */ + private boolean skipTokenCheck = false; + } + @Data public static class Sync { diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenInterceptor.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenInterceptor.java new file mode 100644 index 0000000..8a427f8 --- /dev/null +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenInterceptor.java @@ -0,0 +1,74 @@ +package com.datamate.knowledgegraph.infrastructure.security; + +import com.datamate.common.infrastructure.common.Response; +import com.datamate.knowledgegraph.infrastructure.exception.KnowledgeGraphErrorCode; +import com.datamate.knowledgegraph.infrastructure.neo4j.KnowledgeGraphProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.io.IOException; + +/** + * 内部服务调用 Token 校验拦截器。 + *

+ * 验证 {@code X-Internal-Token} 请求头,保护 sync 端点仅供内部服务/定时任务调用。 + *

+ * 安全策略(fail-closed): + *

+ */ +@Component +@RequiredArgsConstructor +public class InternalTokenInterceptor implements HandlerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(InternalTokenInterceptor.class); + private static final String HEADER_INTERNAL_TOKEN = "X-Internal-Token"; + + private final KnowledgeGraphProperties properties; + private final ObjectMapper objectMapper; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws IOException { + KnowledgeGraphProperties.Security security = properties.getSecurity(); + String configuredToken = security.getInternalToken(); + + if (!StringUtils.hasText(configuredToken)) { + if (security.isSkipTokenCheck()) { + log.warn("内部调用 Token 未配置且 skip-token-check=true,跳过校验(仅限 dev/test 环境)。"); + return true; + } + log.error("内部调用 Token 未配置且 skip-token-check=false(fail-closed),拒绝请求。" + + "请设置 KG_INTERNAL_TOKEN 环境变量或在 dev/test 环境启用 skip-token-check。"); + writeErrorResponse(response); + return false; + } + + String requestToken = request.getHeader(HEADER_INTERNAL_TOKEN); + + if (!configuredToken.equals(requestToken)) { + writeErrorResponse(response); + return false; + } + + return true; + } + + private void writeErrorResponse(HttpServletResponse response) throws IOException { + Response errorBody = Response.error(KnowledgeGraphErrorCode.UNAUTHORIZED_INTERNAL_CALL); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorBody)); + } +} diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenWebMvcConfigurer.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenWebMvcConfigurer.java new file mode 100644 index 0000000..ec7456c --- /dev/null +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenWebMvcConfigurer.java @@ -0,0 +1,22 @@ +package com.datamate.knowledgegraph.infrastructure.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 注册 {@link InternalTokenInterceptor},仅拦截 sync 端点。 + */ +@Configuration +@RequiredArgsConstructor +public class InternalTokenWebMvcConfigurer implements WebMvcConfigurer { + + private final InternalTokenInterceptor internalTokenInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(internalTokenInterceptor) + .addPathPatterns("/knowledge-graph/*/sync/**"); + } +} diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/interfaces/rest/GraphSyncController.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/interfaces/rest/GraphSyncController.java index 0d19fd0..291bfef 100644 --- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/interfaces/rest/GraphSyncController.java +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/interfaces/rest/GraphSyncController.java @@ -23,10 +23,13 @@ import java.util.List; * 提供手动触发 MySQL → Neo4j 同步的 REST 端点。 * 生产环境中也可通过定时任务自动触发。 *

- * 安全说明:本接口仅供内部服务调用(API Gateway / 定时任务), - * 外部请求必须经 API Gateway 鉴权后转发。 - * 生产环境建议通过 mTLS 或内部 JWT 进一步加固服务间认证。 - * 当前通过 {@code X-Internal-Token} 请求头进行简单的内部调用校验。 + * 安全架构: + *

+ * Token 校验由 {@code InternalTokenInterceptor} 拦截器统一实现, + * 对 {@code /knowledge-graph/{graphId}/sync/} 路径前缀自动生效。 */ @RestController @RequestMapping("/knowledge-graph/{graphId}/sync") diff --git a/backend/services/knowledge-graph-service/src/main/resources/application-knowledgegraph.yml b/backend/services/knowledge-graph-service/src/main/resources/application-knowledgegraph.yml index 4a02bc6..45dcfe3 100644 --- a/backend/services/knowledge-graph-service/src/main/resources/application-knowledgegraph.yml +++ b/backend/services/knowledge-graph-service/src/main/resources/application-knowledgegraph.yml @@ -23,6 +23,14 @@ datamate: max-nodes-per-query: ${KG_MAX_NODES:500} # 批量导入批次大小 import-batch-size: ${KG_IMPORT_BATCH_SIZE:100} + # 安全配置 + security: + # 内部服务调用 Token(用于 sync 端点的 X-Internal-Token 校验) + # 生产环境务必通过 KG_INTERNAL_TOKEN 环境变量设置,否则 sync 端点将拒绝所有请求(fail-closed) + internal-token: ${KG_INTERNAL_TOKEN:} + # 是否跳过 Token 校验(默认 false = fail-closed) + # 仅在 dev/test 环境显式设置为 true 以跳过校验 + skip-token-check: ${KG_SKIP_TOKEN_CHECK:false} # MySQL → Neo4j 同步配置 sync: # 数据管理服务地址 diff --git a/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenInterceptorTest.java b/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenInterceptorTest.java new file mode 100644 index 0000000..6124dcf --- /dev/null +++ b/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/infrastructure/security/InternalTokenInterceptorTest.java @@ -0,0 +1,152 @@ +package com.datamate.knowledgegraph.infrastructure.security; + +import com.datamate.knowledgegraph.infrastructure.neo4j.KnowledgeGraphProperties; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class InternalTokenInterceptorTest { + + private static final String VALID_TOKEN = "test-secret-token"; + + private KnowledgeGraphProperties properties; + private InternalTokenInterceptor interceptor; + + @BeforeEach + void setUp() { + properties = new KnowledgeGraphProperties(); + interceptor = new InternalTokenInterceptor(properties, new ObjectMapper()); + } + + // ----------------------------------------------------------------------- + // fail-closed:Token 未配置 + skipTokenCheck=false → 拒绝 + // ----------------------------------------------------------------------- + + @Test + void tokenNotConfigured_skipFalse_rejects() throws Exception { + properties.getSecurity().setInternalToken(null); + properties.getSecurity().setSkipTokenCheck(false); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertThat(result).isFalse(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + assertThat(response.getContentAsString()).contains("knowledge_graph.0013"); + } + + @Test + void tokenEmpty_skipFalse_rejects() throws Exception { + properties.getSecurity().setInternalToken(""); + properties.getSecurity().setSkipTokenCheck(false); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertThat(result).isFalse(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + // ----------------------------------------------------------------------- + // dev/test 放行:Token 未配置 + skipTokenCheck=true → 放行 + // ----------------------------------------------------------------------- + + @Test + void tokenNotConfigured_skipTrue_allows() throws Exception { + properties.getSecurity().setInternalToken(null); + properties.getSecurity().setSkipTokenCheck(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertThat(result).isTrue(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + // ----------------------------------------------------------------------- + // 正常校验:Token 已配置 + 请求头匹配 → 放行 + // ----------------------------------------------------------------------- + + @Test + void validToken_allows() throws Exception { + properties.getSecurity().setInternalToken(VALID_TOKEN); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Internal-Token", VALID_TOKEN); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertThat(result).isTrue(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + // ----------------------------------------------------------------------- + // 401:Token 已配置 + 请求头不匹配 → 拒绝 + // ----------------------------------------------------------------------- + + @Test + void invalidToken_rejects() throws Exception { + properties.getSecurity().setInternalToken(VALID_TOKEN); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Internal-Token", "wrong-token"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertThat(result).isFalse(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + assertThat(response.getContentType()).startsWith("application/json"); + assertThat(response.getContentAsString()).contains("knowledge_graph.0013"); + } + + @Test + void missingTokenHeader_rejects() throws Exception { + properties.getSecurity().setInternalToken(VALID_TOKEN); + + MockHttpServletRequest request = new MockHttpServletRequest(); + // No X-Internal-Token header + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertThat(result).isFalse(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + // ----------------------------------------------------------------------- + // 错误响应格式:应使用 Response 体系 + // ----------------------------------------------------------------------- + + @Test + void errorResponse_usesResponseFormat() throws Exception { + properties.getSecurity().setInternalToken(VALID_TOKEN); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Internal-Token", "wrong"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + interceptor.preHandle(request, response, new Object()); + + String body = response.getContentAsString(); + assertThat(body).contains("\"code\""); + assertThat(body).contains("\"message\""); + // Response.error() 包含 data 字段(值为 null) + assertThat(body).contains("\"data\""); + } +} diff --git a/backend/services/main-application/src/main/java/com/datamate/main/config/SecurityConfig.java b/backend/services/main-application/src/main/java/com/datamate/main/config/SecurityConfig.java index 8e04562..8ff5821 100644 --- a/backend/services/main-application/src/main/java/com/datamate/main/config/SecurityConfig.java +++ b/backend/services/main-application/src/main/java/com/datamate/main/config/SecurityConfig.java @@ -7,8 +7,14 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.web.SecurityFilterChain; /** - * 安全配置 - 暂时禁用所有认证 - * 开发阶段使用,生产环境需要启用认证 + * Spring Security 配置。 + *

+ * 安全架构采用双层防护: + *

+ * 当前 SecurityFilterChain 配置为 permitAll,HTTP 级别的访问控制由 Gateway 和业务拦截器共同完成。 */ @Configuration @EnableWebSecurity diff --git a/backend/services/main-application/src/main/resources/application.yml b/backend/services/main-application/src/main/resources/application.yml index 227f995..db84873 100644 --- a/backend/services/main-application/src/main/resources/application.yml +++ b/backend/services/main-application/src/main/resources/application.yml @@ -3,12 +3,6 @@ spring: application: name: datamate - # 暂时排除Spring Security自动配置(开发阶段使用) - autoconfigure: - exclude: - - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration - - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration - # 数据源配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver