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

  1. Regenerate nonce (and timestamp + sign) for every request attempt — never reuse a nonce on retry
  2. Disable or configure HTTP client auto-retry so it does not silently replay signed requests
  3. If the resend is intentional, wait until the nonce TTL (2x signature.timeout) expires, or use a different nonce
  4. 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

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.