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());
+    }
 }

Reply via email to