diff --git a/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java b/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java index 9f8a4b3..8502dd5 100644 --- a/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java +++ b/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java @@ -101,13 +101,11 @@ public class WebMvcConfig implements WebMvcConfigurer { "/api/v1/masterKey/recover", "/api/v1/auth/password-login", "/api/v1/system/health", - "/api/v1/device/time-config", "/api/v1/auth/ukey-login", "/api/v1/init/template", "/api/v1/auth/ukey-login/randoms", "/api/v1/auth/captcha", "/api/v1/auth/super-admin/ukeys/issue-sign", - "/api/v1/device/network/**", "/api/v1/masterKey/activate", "/api/v1/masterKey/activate/info" ); diff --git a/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java b/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java index 1319477..2876959 100644 --- a/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java +++ b/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java @@ -33,6 +33,9 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor { if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } + if (Boolean.TRUE.equals(request.getAttribute(InternalApiAuthInterceptor.ATTR_BOOTSTRAP_AUTH_BYPASS))) { + return true; + } RequireInternalAuth requireInternalAuth = findAnnotation(handlerMethod, RequireInternalAuth.class); if (requireInternalAuth != null) { diff --git a/src/main/java/com/cisd/tms/modules/auth/security/RequireInternalAuth.java b/src/main/java/com/cisd/tms/modules/auth/security/RequireInternalAuth.java index 1461040..c57bd36 100644 --- a/src/main/java/com/cisd/tms/modules/auth/security/RequireInternalAuth.java +++ b/src/main/java/com/cisd/tms/modules/auth/security/RequireInternalAuth.java @@ -16,4 +16,6 @@ public @interface RequireInternalAuth { RoleCode[] anyRole() default {}; AuthLevel authLevel(); + + boolean allowBeforeMasterKeyInitialized() default false; } diff --git a/src/main/java/com/cisd/tms/modules/device/controller/NetworkConfigController.java b/src/main/java/com/cisd/tms/modules/device/controller/NetworkConfigController.java index b7af1fc..e64a6ac 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/NetworkConfigController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/NetworkConfigController.java @@ -1,5 +1,8 @@ package com.cisd.tms.modules.device.controller; import com.cisd.tms.common.api.ApiResponse; +import com.cisd.tms.modules.auth.enums.AuthLevel; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.security.RequireInternalAuth; import com.cisd.tms.modules.device.dto.network.*; import com.cisd.tms.modules.device.entity.PageResult; import com.cisd.tms.modules.device.service.NetworkConfigService; @@ -17,6 +20,7 @@ import java.util.List; @Tag(name = "网络配置管理", description = "提供设备网络信息查询、IPv4/IPv6配置、Bond配置及路由管理等接口") @RestController @RequestMapping("/api/v1/device/network") +@RequireInternalAuth(role = RoleCode.OPS_ADMIN, authLevel = AuthLevel.FULL, allowBeforeMasterKeyInitialized = true) public class NetworkConfigController { private final NetworkConfigService networkConfigService; diff --git a/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java b/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java index 5bda5f6..7f12273 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java @@ -2,7 +2,6 @@ package com.cisd.tms.modules.device.controller; import com.cisd.tms.common.api.ApiResponse; -import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.modules.auth.enums.AuthLevel; import com.cisd.tms.modules.auth.enums.RoleCode; import com.cisd.tms.modules.auth.security.RequireInternalAuth; @@ -11,7 +10,6 @@ import com.cisd.tms.modules.device.service.TimeConfigService; import com.cisd.tms.modules.log.annotation.AuditedOperation; import com.cisd.tms.modules.log.enums.ActionType; import com.cisd.tms.modules.log.enums.ModuleCode; -import com.cisd.tms.security.internal.ReplayProtected; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; @@ -28,11 +26,10 @@ public class TimeConfigController { private final TimeConfigService timeConfigService; @PostMapping("/time-config") + @RequireInternalAuth(role = RoleCode.OPS_ADMIN, authLevel = AuthLevel.FULL, allowBeforeMasterKeyInitialized = true) @AuditedOperation(module = ModuleCode.DEVICE, action = ActionType.UPDATE, summary = "配置系统时间") public ApiResponse configureTime(@RequestBody TimeConfigRequest request) { - //todo 鉴权 - timeConfigService.processTimeConfig(request); - - return ApiResponse.success(); + timeConfigService.processTimeConfig(request); + return ApiResponse.success(); } } diff --git a/src/main/java/com/cisd/tms/security/internal/InternalApiAuthInterceptor.java b/src/main/java/com/cisd/tms/security/internal/InternalApiAuthInterceptor.java index 2aca9b3..9961da0 100644 --- a/src/main/java/com/cisd/tms/security/internal/InternalApiAuthInterceptor.java +++ b/src/main/java/com/cisd/tms/security/internal/InternalApiAuthInterceptor.java @@ -4,7 +4,9 @@ import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.common.config.properties.TmsSecurityProperties; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.util.HttpResponseUtil; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import com.cisd.tms.modules.auth.security.RequireInternalAuth; import com.cisd.tms.modules.auth.repository.AuthSessionRepository; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -15,9 +17,10 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.stereotype.Component; import org.springframework.web.cors.CorsUtils; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @Component @@ -27,6 +30,7 @@ public class InternalApiAuthInterceptor implements HandlerInterceptor { public static final String ATTR_ROLE_CODE = "CURRENT_ROLE_CODE"; public static final String ATTR_AUTH_LEVEL = "CURRENT_AUTH_LEVEL"; public static final String ATTR_SESSION_TOKEN = "CURRENT_SESSION_TOKEN"; + public static final String ATTR_BOOTSTRAP_AUTH_BYPASS = "BOOTSTRAP_AUTH_BYPASS"; private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String SESSION_HEADER = "X-Session-Token"; @@ -37,6 +41,7 @@ public class InternalApiAuthInterceptor implements HandlerInterceptor { private final TmsSecurityProperties securityProperties; private final ObjectMapper objectMapper; private final Clock clock; + private final PcieCryptoService pcieCryptoService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { @@ -51,6 +56,11 @@ public class InternalApiAuthInterceptor implements HandlerInterceptor { return true; } + if (allowBootstrapWithoutSession(handler)) { + request.setAttribute(ATTR_BOOTSTRAP_AUTH_BYPASS, Boolean.TRUE); + return true; + } + String sessionToken = extractSessionToken(request); if (sessionToken == null || sessionToken.isBlank()) { writeSessionInvalid(response, "会话令牌缺失"); @@ -89,6 +99,24 @@ public class InternalApiAuthInterceptor implements HandlerInterceptor { return null; } + private boolean allowBootstrapWithoutSession(Object handler) { + if (!(handler instanceof HandlerMethod handlerMethod)) { + return false; + } + RequireInternalAuth requireInternalAuth = findAnnotation(handlerMethod, RequireInternalAuth.class); + return requireInternalAuth != null + && requireInternalAuth.allowBeforeMasterKeyInitialized() + && !pcieCryptoService.checkLmk(); + } + + private T findAnnotation(HandlerMethod handlerMethod, Class annotationType) { + T methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType); + if (methodAnnotation != null) { + return methodAnnotation; + } + return AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), annotationType); + } + private LocalDateTime now() { return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC); } diff --git a/src/main/resources/db/migration/V1__tms_schema_full.sql b/src/main/resources/db/migration/V1__tms_schema_full.sql index f7ca887..6dc4dcd 100644 --- a/src/main/resources/db/migration/V1__tms_schema_full.sql +++ b/src/main/resources/db/migration/V1__tms_schema_full.sql @@ -667,7 +667,7 @@ CREATE TABLE IF NOT EXISTS tms_certificate_crl_import_task ( CREATE TABLE IF NOT EXISTS tms_operation_audit_log ( id BIGINT PRIMARY KEY, log_id VARCHAR(64) NOT NULL COMMENT '日志唯一标识', - operator_role_code VARCHAR(64) NOT NULL COMMENT '操作角色编码', + operator_role_code VARCHAR(64) NULL COMMENT '操作角色编码', operator_auth_level VARCHAR(32) NULL COMMENT '操作时认证等级', module_code VARCHAR(32) NOT NULL COMMENT '业务模块', action_type VARCHAR(32) NOT NULL COMMENT '操作动作', diff --git a/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java b/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java index f8220e8..6688942 100644 --- a/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java @@ -94,6 +94,20 @@ class InternalAuthorizationInterceptorTest { Mockito.verifyNoInteractions(operationAuditService); } + @Test + void shouldAllowBootstrapBypassBeforeRoleAuthorization() throws Exception { + OperationAuditService operationAuditService = Mockito.mock(OperationAuditService.class); + InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper(), operationAuditService); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/device/time-config"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_BOOTSTRAP_AUTH_BYPASS, Boolean.TRUE); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, handler("bootstrapOpsEndpoint")); + + Assertions.assertTrue(allowed); + Mockito.verifyNoInteractions(operationAuditService); + } + private static HandlerMethod handler(String methodName) throws NoSuchMethodException { DemoController controller = new DemoController(); return new HandlerMethod(controller, DemoController.class.getDeclaredMethod(methodName)); @@ -119,5 +133,13 @@ class InternalAuthorizationInterceptorTest { ) public void opsOrSuperAdminFullEndpoint() { } + + @RequireInternalAuth( + role = com.cisd.tms.modules.auth.enums.RoleCode.OPS_ADMIN, + authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.FULL, + allowBeforeMasterKeyInitialized = true + ) + public void bootstrapOpsEndpoint() { + } } } diff --git a/src/test/java/com/cisd/tms/security/internal/InternalApiAuthInterceptorTest.java b/src/test/java/com/cisd/tms/security/internal/InternalApiAuthInterceptorTest.java index 81a5594..3f56a6a 100644 --- a/src/test/java/com/cisd/tms/security/internal/InternalApiAuthInterceptorTest.java +++ b/src/test/java/com/cisd/tms/security/internal/InternalApiAuthInterceptorTest.java @@ -3,7 +3,10 @@ package com.cisd.tms.security.internal; import com.cisd.tms.common.config.properties.TmsSecurityProperties; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import com.cisd.tms.modules.auth.enums.AuthLevel; +import com.cisd.tms.modules.auth.security.RequireInternalAuth; import com.cisd.tms.modules.auth.repository.AuthSessionRepository; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Clock; import java.time.Instant; @@ -15,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.method.HandlerMethod; class InternalApiAuthInterceptorTest { @@ -27,7 +31,8 @@ class InternalApiAuthInterceptorTest { authSessionRepository, enabledSecurityProperties(), new ObjectMapper(), - FIXED_CLOCK + FIXED_CLOCK, + Mockito.mock(PcieCryptoService.class) ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/auth/me"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -55,7 +60,8 @@ class InternalApiAuthInterceptorTest { authSessionRepository, enabledSecurityProperties(), new ObjectMapper(), - FIXED_CLOCK + FIXED_CLOCK, + Mockito.mock(PcieCryptoService.class) ); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/auth/me"); request.addHeader("X-Session-Token", "token-expired-001"); @@ -69,9 +75,66 @@ class InternalApiAuthInterceptorTest { Assertions.assertTrue(response.getContentAsString().contains("会话已过期")); } + @Test + void shouldBypassSessionForBootstrapEndpointBeforeMasterKeyInitialized() throws Exception { + AuthSessionRepository authSessionRepository = Mockito.mock(AuthSessionRepository.class); + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + Mockito.when(pcieCryptoService.checkLmk()).thenReturn(false); + InternalApiAuthInterceptor interceptor = new InternalApiAuthInterceptor( + authSessionRepository, + enabledSecurityProperties(), + new ObjectMapper(), + FIXED_CLOCK, + pcieCryptoService + ); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/device/time-config"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, handler("bootstrapOpsEndpoint")); + + Assertions.assertTrue(allowed); + Assertions.assertEquals(Boolean.TRUE, request.getAttribute(InternalApiAuthInterceptor.ATTR_BOOTSTRAP_AUTH_BYPASS)); + Assertions.assertNull(request.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN)); + Mockito.verifyNoInteractions(authSessionRepository); + } + + @Test + void shouldRequireSessionForBootstrapEndpointAfterMasterKeyInitialized() throws Exception { + AuthSessionRepository authSessionRepository = Mockito.mock(AuthSessionRepository.class); + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + Mockito.when(pcieCryptoService.checkLmk()).thenReturn(true); + InternalApiAuthInterceptor interceptor = new InternalApiAuthInterceptor( + authSessionRepository, + enabledSecurityProperties(), + new ObjectMapper(), + FIXED_CLOCK, + pcieCryptoService + ); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/device/time-config"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, handler("bootstrapOpsEndpoint")); + + Assertions.assertFalse(allowed); + Assertions.assertEquals(401, response.getStatus()); + Assertions.assertTrue(response.getContentAsString().contains("会话令牌缺失")); + } + private static TmsSecurityProperties enabledSecurityProperties() { TmsSecurityProperties properties = new TmsSecurityProperties(); properties.getInternalAuth().setEnabled(true); return properties; } + + private static HandlerMethod handler(String methodName) throws NoSuchMethodException { + DemoController controller = new DemoController(); + return new HandlerMethod(controller, DemoController.class.getDeclaredMethod(methodName)); + } + + @SuppressWarnings("unused") + private static class DemoController { + @RequireInternalAuth(authLevel = AuthLevel.FULL, allowBeforeMasterKeyInitialized = true) + public void bootstrapOpsEndpoint() { + } + } }