Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@

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;

Expand Down Expand Up @@ -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
Expand All @@ -99,7 +103,11 @@ public Map<String, String> signHeaders(

// 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);

Expand All @@ -111,13 +119,41 @@ public Map<String, String> signHeaders(
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,
DLFToken token,
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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@
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;
Expand Down Expand Up @@ -217,4 +223,87 @@ public void testEmptyHost() {
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());
}
}