YunaiV/ruoyi-vue-pro · warning · ServiceException

900

900

Error message

存在重复请求

What it means

After a valid signature, ApiSignatureAspect stores the request nonce in Redis (setNonce) with TTL = timeout*2 to prevent replay. If setNonce returns false, the nonce was already seen within the window, so the request is a replay and it throws ServiceException(REPEATED_REQUESTS code 900, '存在重复请求'). It is the anti-replay leg of the signature scheme.

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 0418084e22)

Solutions

  1. Generate a fresh, unique nonce (>=10 chars, random) for every request, never reuse.
  2. Ensure retries also regenerate the nonce and re-sign.
  3. Increase @ApiSignature timeout if legitimate latency exceeds the nonce window.
  4. On the client, treat code 900 'duplicate' as terminal for that request id.

Example fix

// before: static/cached nonce reused across retries
String nonce = "abc123";
// after: fresh nonce per request
String nonce = RandomUtil.randomString(16);
Defensive patterns

Strategy: validation

Validate before calling

String nonce = request.getHeader(signature.nonce());
if (nonce == null || nonce.length() < 10) throw new IllegalArgumentException("nonce missing or too short");
if (!signatureRedisDAO.setNonce(appId, nonce, signature.timeout()*2, signature.timeUnit()))
    throw new ServiceException(REPEATED_REQUESTS);

Type guard

static boolean isValidNonce(String n) { return n != null && n.length() >= 10; }

Try / catch

try { aspect.beforePointCut(joinPoint, signature); }
catch (ServiceException e) { if (e.getCode()==900 && "存在重复请求".equals(e.getMessage())) return ResponseEntity.status(409).body("replay"); throw e; }

Prevention

When it happens

Trigger: The same request (same appId+nonce+sign) is sent twice within 2*timeout; a client reuses a nonce instead of generating a fresh random one; an attacker replays a captured request; the nonce TTL is shorter than real network latency so legit retries collide.

Common situations: Client caching/reusing nonces; retry middleware resending identical requests; duplicate network delivery causing the same nonce twice.

Related errors


AI-assisted analysis of YunaiV/ruoyi-vue-pro@0418084e22 (2026-08-14). Data as JSON: /api/errors/710c4017a32f9e73. Report an issue: GitHub.