This is an automated email from the ASF dual-hosted git repository.
lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 913c6d7e09 [rest] Improve RestCatalog OpenAPI nonce generation (#7270)
913c6d7e09 is described below
commit 913c6d7e091b03642018479a351ec3e9240f82d2
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Thu Feb 12 06:28:39 2026 +0800
[rest] Improve RestCatalog OpenAPI nonce generation (#7270)
Improve Paimon REST OpenAPI nonce generation and auth code (Inspired by
aliyun/tea-java approach.):
- Use UUID + timestamp + thread ID for nonce instead of UUID alone,
because UUID can collide in VMs.;
- Replace `SimpleDateFormat` with `DateTimeFormatter`;
- Add null checks in `signHeaders` and `authorization`.
---
.../apache/paimon/rest/auth/DLFOpenApiSigner.java | 58 +++++++++++---
.../paimon/rest/auth/DLFRequestSignerTest.java | 89 ++++++++++++++++++++++
2 files changed, 136 insertions(+), 11 deletions(-)
diff --git
a/paimon-api/src/main/java/org/apache/paimon/rest/auth/DLFOpenApiSigner.java
b/paimon-api/src/main/java/org/apache/paimon/rest/auth/DLFOpenApiSigner.java
index 0e109dd62f..80f8761850 100644
--- a/paimon-api/src/main/java/org/apache/paimon/rest/auth/DLFOpenApiSigner.java
+++ b/paimon-api/src/main/java/org/apache/paimon/rest/auth/DLFOpenApiSigner.java
@@ -26,13 +26,12 @@ import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
-import java.text.SimpleDateFormat;
import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashMap;
-import java.util.Locale;
import java.util.Map;
-import java.util.TimeZone;
import java.util.TreeMap;
import java.util.UUID;
@@ -65,20 +64,25 @@ public class DLFOpenApiSigner implements DLFRequestSigner {
private static final String SIGNATURE_VERSION_VALUE = "1.0";
private static final String API_VERSION = "2026-01-18";
- private static final SimpleDateFormat GMT_DATE_FORMATTER =
- new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'",
Locale.ENGLISH);
-
- static {
- GMT_DATE_FORMATTER.setTimeZone(TimeZone.getTimeZone("GMT"));
- }
+ private static final DateTimeFormatter GMT_DATE_FORMATTER =
+ DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'")
+ .withZone(ZoneId.of("GMT"));
@Override
public Map<String, String> signHeaders(
@Nullable String body, Instant now, @Nullable String
securityToken, String host) {
+ // Parameter validation
+ if (now == null) {
+ throw new IllegalArgumentException("Parameter 'now' cannot be
null");
+ }
+ if (host == null) {
+ throw new IllegalArgumentException("Parameter 'host' cannot be
null");
+ }
+
Map<String, String> headers = new HashMap<>();
// Date header (GMT format)
- String dateStr = GMT_DATE_FORMATTER.format(java.util.Date.from(now));
+ String dateStr =
GMT_DATE_FORMATTER.format(now.atZone(ZoneId.of("GMT")));
headers.put(DATE_HEADER, dateStr);
// Accept header
@@ -99,7 +103,11 @@ public class DLFOpenApiSigner implements DLFRequestSigner {
// x-acs-* headers
headers.put(X_ACS_SIGNATURE_METHOD, SIGNATURE_METHOD_VALUE);
- headers.put(X_ACS_SIGNATURE_NONCE, UUID.randomUUID().toString());
+
+ // Enhanced nonce: UUID + timestamp + thread ID
+ String nonce = generateUniqueNonce();
+ headers.put(X_ACS_SIGNATURE_NONCE, nonce);
+
headers.put(X_ACS_SIGNATURE_VERSION, SIGNATURE_VERSION_VALUE);
headers.put(X_ACS_VERSION, API_VERSION);
@@ -111,6 +119,20 @@ public class DLFOpenApiSigner implements DLFRequestSigner {
return headers;
}
+ /**
+ * Generates a unique nonce: UUID + timestamp + thread ID.
+ *
+ * @return unique nonce string
+ */
+ private String generateUniqueNonce() {
+ StringBuilder uniqueNonce = new StringBuilder();
+ UUID uuid = UUID.randomUUID();
+ uniqueNonce.append(uuid.toString());
+ uniqueNonce.append(System.currentTimeMillis());
+ uniqueNonce.append(Thread.currentThread().getId());
+ return uniqueNonce.toString();
+ }
+
@Override
public String authorization(
RESTAuthParameter restAuthParameter,
@@ -118,6 +140,20 @@ public class DLFOpenApiSigner implements DLFRequestSigner {
String host,
Map<String, String> signHeaders)
throws Exception {
+ // Parameter validation
+ if (restAuthParameter == null) {
+ throw new IllegalArgumentException("Parameter 'restAuthParameter'
cannot be null");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("Parameter 'token' cannot be
null");
+ }
+ if (host == null) {
+ throw new IllegalArgumentException("Parameter 'host' cannot be
null");
+ }
+ if (signHeaders == null) {
+ throw new IllegalArgumentException("Parameter 'signHeaders' cannot
be null");
+ }
+
// Step 1: Build CanonicalizedHeaders (x-acs-* headers, sorted,
lowercase)
String canonicalizedHeaders = buildCanonicalizedHeaders(signHeaders);
diff --git
a/paimon-api/src/test/java/org/apache/paimon/rest/auth/DLFRequestSignerTest.java
b/paimon-api/src/test/java/org/apache/paimon/rest/auth/DLFRequestSignerTest.java
index 9a078ae790..ef9d44aa5e 100644
---
a/paimon-api/src/test/java/org/apache/paimon/rest/auth/DLFRequestSignerTest.java
+++
b/paimon-api/src/test/java/org/apache/paimon/rest/auth/DLFRequestSignerTest.java
@@ -23,8 +23,14 @@ import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -217,4 +223,87 @@ public class DLFRequestSignerTest {
assertEquals(
DLFDefaultSigner.IDENTIFIER,
DLFAuthProviderFactory.parseSigningAlgoFromUri(null));
}
+
+ @Test
+ public void testOpenApiSignHeadersWithEnhancedNonce() throws Exception {
+ DLFOpenApiSigner signer = new DLFOpenApiSigner();
+ String body =
"{\"CategoryName\":\"test\",\"CategoryType\":\"UNSTRUCTURED\"}";
+ Instant now = ZonedDateTime.of(2025, 4, 16, 3, 44, 46, 0,
ZoneOffset.UTC).toInstant();
+ String host = "dlfnext.cn-beijing.aliyuncs.com";
+
+ Map<String, String> headers = signer.signHeaders(body, now, null,
host);
+
+ assertNotNull(headers.get("Date"));
+ assertEquals("application/json", headers.get("Accept"));
+ assertNotNull(headers.get("Content-MD5"));
+ assertEquals("application/json", headers.get("Content-Type"));
+ assertEquals(host, headers.get("Host"));
+ assertEquals("HMAC-SHA1", headers.get("x-acs-signature-method"));
+
+ // Verify nonce format inspired by Alibaba Cloud DataLake SDK
+ String nonceValue = headers.get("x-acs-signature-nonce");
+ assertNotNull(nonceValue);
+
+ // Verify nonce contains UUID part (should be 32 hex chars + 4 dashes
= 36 chars)
+ // Find the UUID part by looking for the typical UUID pattern
+ java.util.regex.Pattern uuidPattern =
+ java.util.regex.Pattern.compile(
+
"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
+ java.util.regex.Matcher matcher = uuidPattern.matcher(nonceValue);
+ assertTrue(matcher.find(), "No UUID pattern found in nonce: " +
nonceValue);
+
+ // Verify that nonce contains timestamp-like numbers (long digits)
+ // Should contain millisecond timestamp (at least 10 digits)
+ java.util.regex.Pattern digitPattern =
java.util.regex.Pattern.compile("\\d+");
+ java.util.regex.Matcher digitMatcher =
digitPattern.matcher(nonceValue);
+ boolean timestampFound = false;
+ boolean threadIdFound = false;
+ while (digitMatcher.find()) {
+ String digitSequence = digitMatcher.group();
+ if (digitSequence.length() >= 10) { // At least 10 digits for
timestamp
+ timestampFound = true;
+ }
+ if (digitSequence.length() >= 1) { // Thread ID could be shorter
+ threadIdFound = true;
+ }
+ }
+ assertTrue(timestampFound, "No timestamp-like part found in nonce: " +
nonceValue);
+ assertTrue(threadIdFound, "No thread ID-like part found in nonce: " +
nonceValue);
+
+ assertEquals("1.0", headers.get("x-acs-signature-version"));
+ assertEquals("2026-01-18", headers.get("x-acs-version"));
+ }
+
+ @Test
+ public void testConcurrentNonceGeneration() throws InterruptedException {
+ DLFOpenApiSigner signer = new DLFOpenApiSigner();
+ String body = "{\"test\":\"data\"}";
+ Instant now = Instant.now();
+ String host = "test-host";
+ int threadCount = 10;
+ int iterationsPerThread = 50;
+
+ Set<String> nonces = Collections.synchronizedSet(new HashSet<>());
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+ CountDownLatch latch = new CountDownLatch(threadCount);
+
+ for (int i = 0; i < threadCount; i++) {
+ executor.submit(
+ () -> {
+ for (int j = 0; j < iterationsPerThread; j++) {
+ Map<String, String> headers =
signer.signHeaders(body, now, null, host);
+ String nonce =
headers.get("x-acs-signature-nonce");
+ nonces.add(nonce);
+ }
+ latch.countDown();
+ });
+ }
+
+ latch.await();
+ executor.shutdown();
+
+ // Verify all generated nonces are unique
+ assertEquals((long) threadCount * iterationsPerThread, nonces.size());
+ }
}