{"record":{"id":"7a455f1e5985a627","repo":"YunaiV/yudao-cloud","slug":"900-7a455f","errorCode":"900","errorMessage":"存在重复请求","messagePattern":"存在重复请求","errorType":"exception","errorClass":"ServiceException","httpStatus":null,"severity":"error","filePath":"yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java","lineNumber":77,"sourceCode":"        // 1.2 校验 appId 是否能获取到对应的 appSecret\n        String appId = request.getHeader(signature.appId());\n        String appSecret = signatureRedisDAO.getAppSecret(appId);\n        Assert.notNull(appSecret, \"[appId({})] 找不到对应的 appSecret\", appId);\n\n        // 2. 校验签名【重要！】\n        String clientSignature = request.getHeader(signature.sign()); // 客户端签名\n        String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串\n        String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名\n        if (ObjUtil.notEqual(clientSignature, serverSignature)) {\n            return false;\n        }\n\n        // 3. 将 nonce 记入缓存，防止重复使用（重点二：此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ）\n        String nonce = request.getHeader(signature.nonce());\n        if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit()))) {\n            String timestamp = request.getHeader(signature.timestamp());\n            log.info(\"[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]\", appId, timestamp, nonce, clientSignature);\n            throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), \"存在重复请求\");\n        }\n        return true;\n    }\n\n    /**\n     * 校验请求头加签参数\n     * <p>\n     * 1. appId 是否为空\n     * 2. timestamp 是否为空，请求是否已经超时，默认 10 分钟\n     * 3. nonce 是否为空，随机数是否 10 位以上，是否在规定时间内已经访问过了\n     * 4. sign 是否为空\n     *\n     * @param signature signature\n     * @param request   request\n     * @return 是否校验 Header 通过\n     */\n    private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {\n        // 1. 非空校验","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/YunaiV/yudao-cloud/blob/477be9dd49ab7223a972a6abdff0684d6423dec3/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java#L59-L95","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (nonce reused on retry)\nString nonce = UUID.randomUUID().toString();\nResponse r = retryingClient.call(headers(appId, ts, nonce, sign));\n\n// after (fresh nonce per attempt)\nfor (int i = 0; i < 2; i++) {\n    String nonce = UUID.randomUUID().toString(); // new nonce every attempt\n    long ts = System.currentTimeMillis();\n    String sign = sha256(appId + ts + nonce + body + appSecret);\n    Response r = client.call(headers(appId, ts, nonce, sign));\n    if (r.isSuccess()) break;\n}","handlingStrategy":"retry","validationCode":"// client-side: ensure nonce uniqueness before each send\nif (nonce == null || nonce.length() < 10 || usedNonces.contains(nonce)) {\n    nonce = UUID.randomUUID().toString().replace(\"-\", \"\");\n}\n// ensure timestamp within the server's allowed clock skew\nlong ts = System.currentTimeMillis();\nif (Math.abs(ts - serverTime) > skewMillis) resyncClock();","typeGuard":null,"tryCatchPattern":"// on code 900 REPEATED_REQUESTS: regenerate nonce+timestamp+sign and retry once\ntry {\n    resp = client.call(sign(appId, ts, freshNonce(), body));\n} catch (ReplayedException e) { // HTTP body code == 900\n    resp = client.call(sign(appId, now(), freshNonce(), body)); // never reuse nonce\n}","preventionTips":["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"],"tags":["api-signature","replay-protection","redis","security","nonce"],"backgroundTag":null,"analyzedSha":"477be9dd49ab7223a972a6abdff0684d6423dec3","analyzedAt":"2026-08-14T13:35:31.121Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}