diff --git a/CHANGES.md b/CHANGES.md index e43b94ff..9d799dbb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ ## 📅 CHANGES +## Version 2.7.5 (2026-11-24) + +- ServiceError.ref is now sanitized to null if it is blank or empty, preventing the inclusion of empty or whitespace-only strings in error responses. This change enhances the clarity and consistency + of error reporting by ensuring that only meaningful references are included in the ServiceError object. +- REF275-1: Enforce JJWT to use Jackson 3 +- RpcDelegate and RpcResult: refactoring + ## Version 2.7.4 (2026-10-24) - 🔒 Security patch: Jackson Databind diff --git a/GUIDE.md b/GUIDE.md index 0c91530d..0ac89283 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -24,7 +24,7 @@ October 2011, then to GitLab in Dec 2016, and eventually to GitHub in Sep 2021. org.summerboot jexpress - 2.7.4 + 2.7.5 ``` diff --git a/README.md b/README.md index 4b4e4d07..79197e27 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ - **[Apache Central Repository][2]** - **[mvnrepository.com][3]** -[1]: https://central.sonatype.com/artifact/org.summerboot/jexpress/2.7.4 -[2]: https://repo.maven.apache.org/maven2/org/summerboot/jexpress/2.7.4 -[3]: https://mvnrepository.com/artifact/org.summerboot/jexpress/2.7.4 +[1]: https://central.sonatype.com/artifact/org.summerboot/jexpress/2.7.5 +[2]: https://repo.maven.apache.org/maven2/org/summerboot/jexpress/2.7.5 +[3]: https://mvnrepository.com/artifact/org.summerboot/jexpress/2.7.5 [View Changelog (CHANGES)](CHANGES.md) diff --git "a/README_\344\270\255\346\226\207.md" "b/README_\344\270\255\346\226\207.md" index 563b7036..65203976 100644 --- "a/README_\344\270\255\346\226\207.md" +++ "b/README_\344\270\255\346\226\207.md" @@ -2,9 +2,9 @@ - **[Maven Central Repository][2]** - **[mvnrepository.com][3]** -[1]: https://repo.maven.apache.org/maven2/org/summerboot/jexpress/2.7.4 -[2]: https://central.sonatype.com/artifact/org.summerboot/jexpress/2.7.4 -[3]: https://mvnrepository.com/artifact/org.summerboot/jexpress/2.7.4 +[1]: https://repo.maven.apache.org/maven2/org/summerboot/jexpress/2.7.5 +[2]: https://central.sonatype.com/artifact/org.summerboot/jexpress/2.7.5 +[3]: https://mvnrepository.com/artifact/org.summerboot/jexpress/2.7.5 [View Changelog (CHANGES)](CHANGES.md) diff --git a/pom.xml b/pom.xml index 9d4d31e9..abfd650d 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.summerboot jexpress - 2.7.4 + 2.7.5-SNAPSHOT jar jExpress ${project.version} jExpress focuses on addressing non-functional and operational maintainability needs, some of which Spring Boot may not yet provide. @@ -217,7 +217,7 @@ 7.0.0 - 7.4.13.Final + 7.4.14.Final 7.1.0 @@ -237,7 +237,7 @@ 3.0.8 - 1.1.100 + 1.1.105 1.20 9.8.0 @@ -356,17 +356,35 @@ io.jsonwebtoken jjwt-api ${jwt.version} + + + com.fasterxml.jackson.core + * + + io.jsonwebtoken jjwt-impl ${jwt.version} + + + com.fasterxml.jackson.core + * + + io.jsonwebtoken jjwt-jackson ${jwt.version} + + + com.fasterxml.jackson.core + * + + diff --git a/src/main/java/org/summerboot/jexpress/api/common/ServiceError.java b/src/main/java/org/summerboot/jexpress/api/common/ServiceError.java index d0c439ce..cebcd0ec 100644 --- a/src/main/java/org/summerboot/jexpress/api/common/ServiceError.java +++ b/src/main/java/org/summerboot/jexpress/api/common/ServiceError.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; +import org.apache.commons.lang3.StringUtils; import org.summerboot.jexpress.boot.BootConstants; import org.summerboot.jexpress.util.lang.BeanUtil; import org.summerboot.jexpress.webserver.jackson.AdditionalJsonFields; @@ -48,7 +49,7 @@ public ServiceError() { } public ServiceError(String ref) { - this.ref = ref; + this.ref = StringUtils.isBlank(ref) ? null : ref; } // public ServiceError(int errorCode, String errorTag, String errorDesc, Throwable ex) { diff --git a/src/main/java/org/summerboot/jexpress/api/rpc/RpcResult.java b/src/main/java/org/summerboot/jexpress/api/rpc/RpcResult.java index 620b8b28..bd9d828f 100644 --- a/src/main/java/org/summerboot/jexpress/api/rpc/RpcResult.java +++ b/src/main/java/org/summerboot/jexpress/api/rpc/RpcResult.java @@ -54,25 +54,24 @@ public class RpcResult { protected T successResponse; protected final ContentType contentType; protected final boolean isStreaming; + protected final SessionContext context; enum ContentType {JSON, XML, OTHER} protected ObjectMapper httpClientConfiguredObjectMapper = null; + @Deprecated(since = "2.7.4", forRemoval = false) public RpcResult(HttpRequest originRequest, String originRequestBody, HttpResponse httpResponse, boolean remoteSuccess) { - this(originRequest, originRequestBody, httpResponse, remoteSuccess, null); + this(originRequest, originRequestBody, httpResponse, remoteSuccess, false, null, null); } - public RpcResult(HttpRequest originRequest, String originRequestBody, HttpResponse httpResponse, boolean remoteSuccess, HttpClientConfig httpClientConfig) { - this(originRequest, originRequestBody, httpResponse, remoteSuccess, false, httpClientConfig); - } - - public RpcResult(HttpRequest originRequest, String originRequestBody, HttpResponse httpResponse, boolean remoteSuccess, boolean isStreaming, HttpClientConfig httpClientConfig) { + public RpcResult(HttpRequest originRequest, String originRequestBody, HttpResponse httpResponse, boolean remoteSuccess, boolean isStreaming, HttpClientConfig httpClientConfig, SessionContext context) { this.originRequest = originRequest; this.originRequestBody = originRequestBody; this.httpResponse = httpResponse; this.rpcResponseBody = httpResponse == null || isStreaming ? null : String.valueOf(httpResponse.body()); this.httpStatusCode = httpResponse == null ? 0 : httpResponse.statusCode(); + this.context = context; this.httpStatus = HttpResponseStatus.valueOf(httpStatusCode); this.remoteSuccess = remoteSuccess; this.isStreaming = isStreaming; @@ -146,23 +145,87 @@ public ContentType contentType() { return contentType; } + /** + * + * @param successResponseClass + * @return + * @deprecated use { @link #deserialize(Class)} + *

Sample code: + *

{@code
+     *     if(rpcResult.isRemoteSuccess()){
+     *         MySuccessObj obj = rpcResult.deserialize(MySuccessObj.class);
+     *     } else {
+     *         MyErrorObj err = rpcResult.deserialize(MyErrorObj.class);
+     *     }
+     * }
+ */ + @Deprecated(since = "2.7.4", forRemoval = false) + public RpcResult update(Class successResponseClass) { + return update(successResponseClass, context); + } + + /** + * + * @param successResponseClass + * @param context + * @return + * @deprecated use { @link #deserialize(Class)} + *

Sample code: + *

{@code
+     *     if(rpcResult.isRemoteSuccess()){
+     *         MySuccessObj obj = rpcResult.deserialize(MySuccessObj.class);
+     *     } else {
+     *         MyErrorObj err = rpcResult.deserialize(MyErrorObj.class);
+     *     }
+     * }
+ */ + @Deprecated(since = "2.7.4", forRemoval = false) + public RpcResult update(Class successResponseClass, final SessionContext context) { + if (remoteSuccess) { + successResponse = deserialize(httpClientConfiguredObjectMapper, null, successResponseClass, true, context); + } + return this; + } + + /** + * + * @param responseClass + * @param context + * @param + * @return + * @deprecated use { @link #deserialize(Class)} + *

Sample code: + *

{@code
+     *     if(rpcResult.isRemoteSuccess()){
+     *         MySuccessObj obj = rpcResult.deserialize(MySuccessObj.class);
+     *     } else {
+     *         MyErrorObj err = rpcResult.deserialize(MyErrorObj.class);
+     *     }
+     * }
+ */ + @Deprecated(since = "2.7.4", forRemoval = false) public R deserialize(Class responseClass, final SessionContext context) { - return deserialize(httpClientConfiguredObjectMapper, responseClass, context); + return deserialize(httpClientConfiguredObjectMapper, null, responseClass, true, context); + } + + public R deserialize(Class responseClass) { + return deserialize(httpClientConfiguredObjectMapper, responseClass); } - public R deserialize(JavaType responseType, final SessionContext context) { - return deserialize(httpClientConfiguredObjectMapper, responseType, context); + public R deserialize(JavaType responseType) { + return deserialize(httpClientConfiguredObjectMapper, responseType); } - public R deserialize(ObjectMapper jacksonMapper, Class responseClass, final SessionContext context) { + public R deserialize(ObjectMapper jacksonMapper, Class responseClass) { return deserialize(jacksonMapper, null, responseClass, true, context); } - public R deserialize(ObjectMapper jacksonMapper, JavaType responseType, final SessionContext context) { + public R deserialize(ObjectMapper jacksonMapper, JavaType responseType) { return deserialize(jacksonMapper, responseType, null, true, context); } - // Debug 现场还原的最大字符数(通常 4KB~8KB 足够看清出错的 JSON 结构了) + + // 💡 for debugging: Maximum number of characters to retrieve for reconstruction (typically 4KB–8KB is sufficient to clearly identify a malformed JSON structure) private static final int SNAPSHOT_SIZE = 8 * 1024; protected R deserialize(ObjectMapper jacksonMapper, JavaType responseType, Class responseClass, boolean doValidation, final SessionContext context) { @@ -174,10 +237,10 @@ protected R deserialize(ObjectMapper jacksonMapper, JavaType responseType, C try { if (isStreaming) { try (InputStream rawInputStream = (InputStream) httpResponse.body()) { - // 💡 1. 创建一个极小的内存口袋,只用来抄录前 8KB 的数据快照 + // 💡 1. Create a tiny memory pocket, used exclusively for recording an 8KB snapshot of the data. ByteArrayOutputStream debugSnapshot = new ByteArrayOutputStream(SNAPSHOT_SIZE); - // 2. 包装原始网络流:在 Jackson 边读边解的过程中,顺便往快照里写一份 + // 💡 2. Wrapping the raw network stream: while Jackson reads and parses the data, simultaneously write a copy to a snapshot. InputStream wrappedStream = new InputStream() { private int bytesCopied = 0; @@ -211,7 +274,9 @@ public int read(byte[] b, int off, int len) throws java.io.IOException { : jacksonMapper.readValue(wrappedStream, responseClass); } catch (JacksonException ex) { String jsonSnapshotStr = debugSnapshot.toString(StandardCharsets.UTF_8); - context.memo(RpcMemo.MEMO_RPC_RESPONSE_DATA, jsonSnapshotStr); + if (context != null) { + context.memo(RpcMemo.MEMO_RPC_RESPONSE_DATA, jsonSnapshotStr); + } throw ex; } } diff --git a/src/main/java/org/summerboot/jexpress/boot/BootConstants.java b/src/main/java/org/summerboot/jexpress/boot/BootConstants.java index 2d75d066..720588da 100644 --- a/src/main/java/org/summerboot/jexpress/boot/BootConstants.java +++ b/src/main/java/org/summerboot/jexpress/boot/BootConstants.java @@ -33,7 +33,7 @@ static boolean isDebugMode() { String APP_ID = String.format("%06d", APP_ID_VALUE); //version - String VERSION = "jExpress 2.7.4"; + String VERSION = "jExpress 2.7.5"; String JEXPRESS_PACKAGE_NAME = "org.summerboot.jexpress"; String JSONFILTER_NAME_SERVICEERROR = "ServiceErrorFilter"; diff --git a/src/main/java/org/summerboot/jexpress/integration/rpc/http/RpcDelegateHttpClientImpl.java b/src/main/java/org/summerboot/jexpress/integration/rpc/http/RpcDelegateHttpClientImpl.java index 385154df..b2ad98fd 100644 --- a/src/main/java/org/summerboot/jexpress/integration/rpc/http/RpcDelegateHttpClientImpl.java +++ b/src/main/java/org/summerboot/jexpress/integration/rpc/http/RpcDelegateHttpClientImpl.java @@ -107,7 +107,7 @@ protected RpcResult rpcEx(SessionContext context, HttpRequest httpRequest Thread.currentThread().interrupt(); Err e = new Err(BootErrorCode.APP_INTERRUPTED, null, "Http Client Interrupted", ex); context.status(HttpResponseStatus.INTERNAL_SERVER_ERROR).error(e); - RpcResult rpcResult = new RpcResult<>(httpRequest, originRequestBody, null, false, getHttpClientConfig()); + RpcResult rpcResult = new RpcResult<>(httpRequest, originRequestBody, null, false, false, getHttpClientConfig(), context); return rpcResult; } finally { context.poi(BootPoi.RPC_END); @@ -128,7 +128,7 @@ protected RpcResult rpcEx(SessionContext context, HttpRequest httpRequest } //3b. update status - RpcResult rpcResult = new RpcResult<>(httpRequest, originRequestBody, isStreaming ? httpResponse2 : httpResponse1, isRemoteSuccess, isStreaming, getHttpClientConfig()); + RpcResult rpcResult = new RpcResult<>(httpRequest, originRequestBody, isStreaming ? httpResponse2 : httpResponse1, isRemoteSuccess, isStreaming, getHttpClientConfig(), context); String rpcResponseJsonBody = rpcResult.httpResponseBody(); context.memo(RpcMemo.MEMO_RPC_RESPONSE, rpcResult.httpStatusCode() + " " + (isStreaming ? httpResponse2.headers() : httpResponse1.headers())); context.memo(RpcMemo.MEMO_RPC_RESPONSE_DATA, isStreaming ? "stream data" : rpcResponseJsonBody); diff --git a/src/main/java/org/summerboot/jexpress/security/token/jwt/Jackson3Deserializer.java b/src/main/java/org/summerboot/jexpress/security/token/jwt/Jackson3Deserializer.java new file mode 100644 index 00000000..5a0882f5 --- /dev/null +++ b/src/main/java/org/summerboot/jexpress/security/token/jwt/Jackson3Deserializer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2005-2026 Du Law Office - jExpress, The Summer Boot Framework Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://apache.org + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.summerboot.jexpress.security.token.jwt; + + +import io.jsonwebtoken.io.DeserializationException; +import io.jsonwebtoken.io.Deserializer; +import tools.jackson.databind.ObjectMapper; + +import java.io.Reader; +import java.util.Map; + +/** + * REF275-1: Enforce JJWT to use Jackson 3 + * + * @author Changski Tie Zheng Zhang 张铁铮, 魏泽北, 杜旺财, 杜富贵 + */ +public class Jackson3Deserializer implements Deserializer> { + + // Use Jackson 3 ObjectMapper for deserialization + private final ObjectMapper objectMapper; + + public Jackson3Deserializer(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + + @Override + public Map deserialize(byte[] bytes) throws DeserializationException { + return objectMapper.readValue(bytes, Map.class); + } + + @Override + public Map deserialize(Reader reader) throws DeserializationException { + return objectMapper.readValue(reader, Map.class); + } +} diff --git a/src/main/java/org/summerboot/jexpress/security/token/jwt/Jackson3Serializer.java b/src/main/java/org/summerboot/jexpress/security/token/jwt/Jackson3Serializer.java new file mode 100644 index 00000000..cd976ebe --- /dev/null +++ b/src/main/java/org/summerboot/jexpress/security/token/jwt/Jackson3Serializer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2005-2026 Du Law Office - jExpress, The Summer Boot Framework Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://apache.org + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.summerboot.jexpress.security.token.jwt; + + +import io.jsonwebtoken.io.SerializationException; +import io.jsonwebtoken.io.Serializer; +import tools.jackson.databind.ObjectMapper; + +import java.io.OutputStream; +import java.util.Map; + +/** + * REF275-1: Enforce JJWT to use Jackson 3 + * + * @author Changski Tie Zheng Zhang 张铁铮, 魏泽北, 杜旺财, 杜富贵 + */ +public class Jackson3Serializer implements Serializer> { + + // Use Jackson 3 ObjectMapper for serialization + private final ObjectMapper objectMapper; + + public Jackson3Serializer(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + + @Override + public byte[] serialize(Map t) throws SerializationException { + return objectMapper.writeValueAsBytes(t); + } + + @Override + public void serialize(Map stringMap, OutputStream out) throws SerializationException { + objectMapper.writeValue(out, stringMap); + } +} diff --git a/src/main/java/org/summerboot/jexpress/security/token/jwt/JwtUtil.java b/src/main/java/org/summerboot/jexpress/security/token/jwt/JwtUtil.java index 12d409f4..f7f8eb3b 100644 --- a/src/main/java/org/summerboot/jexpress/security/token/jwt/JwtUtil.java +++ b/src/main/java/org/summerboot/jexpress/security/token/jwt/JwtUtil.java @@ -25,6 +25,7 @@ import io.jsonwebtoken.security.MacAlgorithm; import io.jsonwebtoken.security.SignatureAlgorithm; import org.summerboot.jexpress.security.EncryptorUtil; +import tools.jackson.databind.ObjectMapper; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -125,8 +126,11 @@ public static KeyPair buildSigningParsingKeyPair(SignatureAlgorithm signatureAlg // return builder.compact(); // } + private final static ObjectMapper jackson3ObjectMapper = new ObjectMapper(); + public static String createJWT(String keyAlgorithm, String jwtSigningKey, int ttlSeconds, String id, String issuer, String subject, Collection audience) { JwtBuilder builder = Jwts.builder() + .json(new Jackson3Serializer(jackson3ObjectMapper)) // REF275-1: Enforce JJWT to use Jackson 3 .id(id) .issuer(issuer) .subject(subject); @@ -141,6 +145,7 @@ public static String createJWT(String keyAlgorithm, String jwtSigningKey, JwtBui public static String createJWT(String keyAlgorithm, byte[] jwtSigningKey, int ttlSeconds, String id, String issuer, String subject, Collection audience) { JwtBuilder builder = Jwts.builder() + .json(new Jackson3Serializer(jackson3ObjectMapper)) // REF275-1: Enforce JJWT to use Jackson 3 .id(id) .issuer(issuer) .subject(subject); @@ -169,6 +174,7 @@ public static void setJwtExpireTime(JwtBuilder builder, Duration ttl) { public static String createJWT(Key privateKey, int ttlSeconds, String id, String issuer, String subject, Collection audience) { JwtBuilder builder = Jwts.builder() + .json(new Jackson3Serializer(jackson3ObjectMapper)) // REF275-1: Enforce JJWT to use Jackson 3 .id(id) .issuer(issuer) .subject(subject); @@ -195,6 +201,7 @@ public static Jws parseJWT(Key verifyKey, String token) { public static Jws parseJWT(SecretKey jwtRootSigningKey, String token) { JwtParser parser = Jwts.parser() // (1) + .json(new Jackson3Deserializer(jackson3ObjectMapper)) // REF275-1: Enforce JJWT to use Jackson 3 .verifyWith(jwtRootSigningKey) // (2) .build(); // (3) return parseJWT(parser, token); // (4) @@ -202,6 +209,7 @@ public static Jws parseJWT(SecretKey jwtRootSigningKey, String token) { public static Jws parseJWT(PublicKey publicKey, String token) { JwtParser parser = Jwts.parser() // (1) + .json(new Jackson3Deserializer(jackson3ObjectMapper)) // REF275-1: Enforce JJWT to use Jackson 3 .verifyWith(publicKey) // (2) .build(); // (3) return parseJWT(parser, token); // (4)