YunaiV/yudao-cloud · error · ServiceException
900
900
Error message
存在重复请求
What it means
Thrown by yudao's API signature protection (ApiSignatureAspect) when a request passes signature verification but its nonce has already been seen within the anti-replay window (TTL = signature.timeout() x 2 in Redis). The aspect deliberately records every nonce after a valid signature check, so any retry or replay of the same signed request within that window is rejected with code 900 REPEATED_REQUESTS. It is a security control, not a bug: the server treats a repeated nonce as a replay attack.
Source
Thrown at yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java:77
// 1.2 校验 appId 是否能获取到对应的 appSecret
String appId = request.getHeader(signature.appId());
String appSecret = signatureRedisDAO.getAppSecret(appId);
Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
// 2. 校验签名【重要!】
String clientSignature = request.getHeader(signature.sign()); // 客户端签名
String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串
String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名
if (ObjUtil.notEqual(clientSignature, serverSignature)) {
return false;
}
// 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
String nonce = request.getHeader(signature.nonce());
if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit()))) {
String timestamp = request.getHeader(signature.timestamp());
log.info("[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]", appId, timestamp, nonce, clientSignature);
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), "存在重复请求");
}
return true;
}
/**
* 校验请求头加签参数
* <p>
* 1. appId 是否为空
* 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟
* 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了
* 4. sign 是否为空
*
* @param signature signature
* @param request request
* @return 是否校验 Header 通过
*/
private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
// 1. 非空校验View on GitHub (pinned to 477be9dd49)
Solutions
- Regenerate nonce (and timestamp + sign) for every request attempt — never reuse a nonce on retry
- Disable or configure HTTP client auto-retry so it does not silently replay signed requests
- If the resend is intentional, wait until the nonce TTL (2x signature.timeout) expires, or use a different nonce
- Make the nonce generator truly random and unique per request (UUID / secure-random 10+ chars), not derived from timestamp alone
Example fix
// before (nonce reused on retry)
String nonce = UUID.randomUUID().toString();
Response r = retryingClient.call(headers(appId, ts, nonce, sign));
// after (fresh nonce per attempt)
for (int i = 0; i < 2; i++) {
String nonce = UUID.randomUUID().toString(); // new nonce every attempt
long ts = System.currentTimeMillis();
String sign = sha256(appId + ts + nonce + body + appSecret);
Response r = client.call(headers(appId, ts, nonce, sign));
if (r.isSuccess()) break;
} Defensive patterns
Strategy: retry
Validate before calling
// client-side: ensure nonce uniqueness before each send
if (nonce == null || nonce.length() < 10 || usedNonces.contains(nonce)) {
nonce = UUID.randomUUID().toString().replace("-", "");
}
// ensure timestamp within the server's allowed clock skew
long ts = System.currentTimeMillis();
if (Math.abs(ts - serverTime) > skewMillis) resyncClock(); Try / catch
// on code 900 REPEATED_REQUESTS: regenerate nonce+timestamp+sign and retry once
try {
resp = client.call(sign(appId, ts, freshNonce(), body));
} catch (ReplayedException e) { // HTTP body code == 900
resp = client.call(sign(appId, now(), freshNonce(), body)); // never reuse nonce
} Prevention
- Generate a fresh nonce and timestamp for every request attempt, including retries
- Disable HTTP-client automatic retries for signed API calls
- Use a high-entropy nonce source (UUID/SecureRandom), never timestamp-derived values
- Log appId+nonce on 900 responses to spot which client is replaying
When it happens
Trigger: Calling an @ApiSignature-annotated endpoint twice with the exact same headers (appId, timestamp, nonce, sign) within signature.timeout()*2; client-side retry logic (e.g. HTTP client auto-retry) that reuses the same nonce; a signature helper that caches headers; two concurrent requests signed with the same nonce value.
Common situations: HTTP client with automatic retry enabled (OkHttp retryOnFailure, feign Retryer) replaying the identical signed request after a timeout; a 4xx/5xx response followed by a manual resend of the same headers; dev/testing where the same curl command is re-run quickly; nonce generated from a low-resolution source (e.g. timestamp only) producing collisions.
Related errors
AI-assisted analysis of YunaiV/yudao-cloud@477be9dd49 (2026-08-14).
Data as JSON: /api/errors/7a455f1e5985a627.
Report an issue: GitHub.