fix:long转string

This commit is contained in:
waner 2026-05-06 16:52:41 +08:00
parent 855ea1247b
commit 11e6049ae2
10 changed files with 156 additions and 12 deletions

View File

@ -0,0 +1,18 @@
package com.cisd.tms.common.config;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonLongSerializationConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer longToStringJacksonCustomizer() {
return builder -> {
builder.serializerByType(Long.class, ToStringSerializer.instance);
builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
};
}
}

View File

@ -74,6 +74,11 @@ public class GlobalExceptionHandler {
@ExceptionHandler(ReplayProtectionException.class)
public ResponseEntity<ApiResponse<Void>> handleReplayProtectionException(ReplayProtectionException ex, HttpServletRequest request) {
if (ex.isClientError()) {
return response(HttpStatus.BAD_REQUEST,
ApiResponse.fail(ErrorCode.BAD_REQUEST.getCode(), ex.getMessage())
.withPath(request.getRequestURI()));
}
log.warn("Replay protection 执行失败", ex);
return response(HttpStatus.SERVICE_UNAVAILABLE,
ApiResponse.fail(ErrorCode.SERVICE_UNAVAILABLE.getCode(), "replay protection unavailable")

View File

@ -2,11 +2,31 @@ package com.cisd.tms.modules.security.replay.exception;
public class ReplayProtectionException extends RuntimeException {
private final boolean clientError;
public ReplayProtectionException(String message) {
super(message);
this(message, false);
}
public ReplayProtectionException(String message, Throwable cause) {
this(message, cause, false);
}
private ReplayProtectionException(String message, boolean clientError) {
super(message);
this.clientError = clientError;
}
private ReplayProtectionException(String message, Throwable cause, boolean clientError) {
super(message, cause);
this.clientError = clientError;
}
public static ReplayProtectionException badRequest(String message) {
return new ReplayProtectionException(message, true);
}
public boolean isClientError() {
return clientError;
}
}

View File

@ -103,25 +103,25 @@ public class ReplayProtectionServiceImpl implements ReplayProtectionService {
private void validateRequest(ReplayCheckRequest request) {
if (request == null) {
throw new ReplayProtectionException("replay 请求不能为空");
throw ReplayProtectionException.badRequest("replay 请求不能为空");
}
if (request.getScope() == null) {
throw new ReplayProtectionException("replay scope不能为空");
throw ReplayProtectionException.badRequest("replay scope不能为空");
}
if (isBlank(request.getPrincipalId())) {
throw new ReplayProtectionException("principal id不能为空");
throw ReplayProtectionException.badRequest("principal id不能为空");
}
if (isBlank(request.getNonce())) {
throw new ReplayProtectionException("nonce不能为空");
throw ReplayProtectionException.badRequest("nonce不能为空");
}
if (request.getRequestTimestamp() <= 0L) {
throw new ReplayProtectionException("request timestamp不能为空");
throw ReplayProtectionException.badRequest("request timestamp不能为空");
}
}
private void ensureTimestampWithinWindow(long requestTimestamp, long nowSeconds, long allowedSkewSeconds) {
if (Math.abs(nowSeconds - requestTimestamp) > allowedSkewSeconds) {
throw new ReplayProtectionException("请求时间戳超出允许窗口");
throw ReplayProtectionException.badRequest("请求时间戳超出允许窗口");
}
}

View File

@ -78,8 +78,7 @@ public class InternalApiReplayInterceptor implements HandlerInterceptor {
return false;
}
} catch (ReplayProtectionException ex) {
writeJson(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE, ErrorCode.SERVICE_UNAVAILABLE.getCode(),
"replay protection unavailable");
writeReplayProtectionError(response, ex);
return false;
}
@ -147,4 +146,13 @@ public class InternalApiReplayInterceptor implements HandlerInterceptor {
private void writeJson(HttpServletResponse response, int status, int code, String message) throws IOException {
HttpResponseUtil.writeJson(response, status, ApiResponse.fail(code, message), objectMapper);
}
private void writeReplayProtectionError(HttpServletResponse response, ReplayProtectionException ex) throws IOException {
if (ex.isClientError()) {
writeJson(response, HttpServletResponse.SC_BAD_REQUEST, ErrorCode.BAD_REQUEST.getCode(), ex.getMessage());
return;
}
writeJson(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE, ErrorCode.SERVICE_UNAVAILABLE.getCode(),
"replay protection unavailable");
}
}

View File

@ -97,8 +97,7 @@ public class OpenApiSignAuthInterceptor implements HandlerInterceptor {
return false;
}
} catch (ReplayProtectionException ex) {
writeJson(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
ErrorCode.SERVICE_UNAVAILABLE.getCode(), "replay protection unavailable");
writeReplayProtectionError(response, ex);
return false;
}
}
@ -153,4 +152,13 @@ public class OpenApiSignAuthInterceptor implements HandlerInterceptor {
objectMapper
);
}
private void writeReplayProtectionError(HttpServletResponse response, ReplayProtectionException ex) throws IOException {
if (ex.isClientError()) {
writeJson(response, HttpServletResponse.SC_BAD_REQUEST, ErrorCode.BAD_REQUEST.getCode(), ex.getMessage());
return;
}
writeJson(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
ErrorCode.SERVICE_UNAVAILABLE.getCode(), "replay protection unavailable");
}
}

View File

@ -0,0 +1,28 @@
package com.cisd.tms.common.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import static org.junit.jupiter.api.Assertions.assertEquals;
class JacksonLongSerializationConfigTest {
@Test
void shouldSerializeLongValuesAsJsonStrings() throws Exception {
JacksonLongSerializationConfig config = new JacksonLongSerializationConfig();
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
config.longToStringJacksonCustomizer().customize(builder);
ObjectMapper objectMapper = builder.build();
String json = objectMapper.writeValueAsString(new LongIdPayload(
2049756829261557762L,
2049756829261557763L
));
assertEquals("{\"id\":\"2049756829261557762\",\"primitiveId\":\"2049756829261557763\"}", json);
}
private record LongIdPayload(Long id, long primitiveId) {
}
}

View File

@ -180,7 +180,8 @@ class ReplayProtectionServiceTest {
ReplayCheckRequest request = request(ReplayScope.OPENAPI, "demo-app", "nonce-old", EXPIRED_REQUEST_TIMESTAMP,
"POST", "/openapi/v1/demo", "abc123");
Assertions.assertThrows(ReplayProtectionException.class, () -> service.check(request));
ReplayProtectionException ex = Assertions.assertThrows(ReplayProtectionException.class, () -> service.check(request));
Assertions.assertTrue(ex.isClientError());
Mockito.verifyNoInteractions(replayNonceRepository);
Mockito.verifyNoInteractions(securityEventRepository);
}

View File

@ -157,6 +157,32 @@ class InternalApiReplayInterceptorTest {
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.SERVICE_UNAVAILABLE.getCode()));
}
@Test
void shouldReturnBadRequestWhenReplayRequestIsInvalid() throws Exception {
ReplayProtectionService replayProtectionService = Mockito.mock(ReplayProtectionService.class);
Mockito.when(replayProtectionService.check(Mockito.any(ReplayCheckRequest.class)))
.thenThrow(ReplayProtectionException.badRequest("请求时间戳超出允许窗口"));
InternalApiReplayInterceptor interceptor = new InternalApiReplayInterceptor(
replayProtectionService,
new TmsSecurityProperties(),
new ObjectMapper(),
FIXED_CLOCK
);
MockHttpServletRequest sourceRequest = new MockHttpServletRequest("POST", "/api/v1/init/tasks/task-001/execute");
sourceRequest.addHeader("X-Request-Timestamp", Long.toString(FIXED_REQUEST_TIMESTAMP - 3600L));
sourceRequest.addHeader("X-Request-Nonce", "nonce-001");
CachedBodyHttpServletRequest request = new CachedBodyHttpServletRequest(sourceRequest);
request.setAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "session-001");
MockHttpServletResponse response = new MockHttpServletResponse();
boolean allowed = interceptor.preHandle(request, response, protectedHandler("execute"));
Assertions.assertFalse(allowed);
Assertions.assertEquals(400, response.getStatus());
Assertions.assertTrue(response.getContentAsString().contains("请求时间戳超出允许窗口"));
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.BAD_REQUEST.getCode()));
}
@Test
void shouldSkipReplayProtectionWhenDisabled() throws Exception {
ReplayProtectionService replayProtectionService = Mockito.mock(ReplayProtectionService.class);

View File

@ -151,6 +151,36 @@ class OpenApiSignAuthInterceptorTest {
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.SERVICE_UNAVAILABLE.getCode()));
}
@Test
void shouldReturnBadRequestWhenReplayRequestIsInvalid() throws Exception {
TmsSecurityProperties securityProperties = securityProperties("demo-app", "demo-secret", 300L);
ReplayProtectionService replayProtectionService = Mockito.mock(ReplayProtectionService.class);
Mockito.when(replayProtectionService.check(Mockito.any(ReplayCheckRequest.class)))
.thenThrow(ReplayProtectionException.badRequest("请求时间戳超出允许窗口"));
OpenApiSignAuthInterceptor interceptor = new OpenApiSignAuthInterceptor(
securityProperties,
replayProtectionService,
new ObjectMapper(),
FIXED_CLOCK
);
MockHttpServletRequest request = signedRequest(
"demo-app",
"demo-secret",
FIXED_NOW_SECONDS,
"nonce-001",
"POST",
"/openapi/v1/demo"
);
MockHttpServletResponse response = new MockHttpServletResponse();
boolean allowed = interceptor.preHandle(request, response, new Object());
Assertions.assertFalse(allowed);
Assertions.assertEquals(400, response.getStatus());
Assertions.assertTrue(response.getContentAsString().contains("请求时间戳超出允许窗口"));
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.BAD_REQUEST.getCode()));
}
@Test
void shouldSkipTimestampWindowAndNonceClaimWhenReplayProtectionIsDisabled() throws Exception {
TmsSecurityProperties securityProperties = securityProperties("demo-app", "demo-secret", 300L);