fix:权限拦截增加主密钥生成前后判断

This commit is contained in:
waner 2026-05-25 14:47:18 +08:00
parent 722106edc7
commit 9a1c4b8354
9 changed files with 129 additions and 12 deletions

View File

@ -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"
);

View File

@ -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) {

View File

@ -16,4 +16,6 @@ public @interface RequireInternalAuth {
RoleCode[] anyRole() default {};
AuthLevel authLevel();
boolean allowBeforeMasterKeyInitialized() default false;
}

View File

@ -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;

View File

@ -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<Void> configureTime(@RequestBody TimeConfigRequest request) {
//todo 鉴权
timeConfigService.processTimeConfig(request);
return ApiResponse.success();
}
}

View File

@ -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 extends java.lang.annotation.Annotation> T findAnnotation(HandlerMethod handlerMethod, Class<T> 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);
}

View File

@ -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 '操作动作',

View File

@ -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() {
}
}
}

View File

@ -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() {
}
}
}