{"record":{"id":"710c4017a32f9e73","repo":"YunaiV/ruoyi-vue-pro","slug":"900-710c40","errorCode":"900","errorMessage":"存在重复请求","messagePattern":"存在重复请求","errorType":"exception","errorClass":"ServiceException","httpStatus":null,"severity":"warning","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/ruoyi-vue-pro/blob/0418084e222612af2fc1141f566af454f9236ab1/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java#L59-L95","documentation":"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.","triggerScenarios":"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.","commonSituations":"Client caching/reusing nonces; retry middleware resending identical requests; duplicate network delivery causing the same nonce twice.","solutions":["Generate a fresh, unique nonce (>=10 chars, random) for every request, never reuse.","Ensure retries also regenerate the nonce and re-sign.","Increase @ApiSignature timeout if legitimate latency exceeds the nonce window.","On the client, treat code 900 'duplicate' as terminal for that request id."],"exampleFix":"// before: static/cached nonce reused across retries\nString nonce = \"abc123\";\n// after: fresh nonce per request\nString nonce = RandomUtil.randomString(16);","handlingStrategy":"validation","validationCode":"String nonce = request.getHeader(signature.nonce());\nif (nonce == null || nonce.length() < 10) throw new IllegalArgumentException(\"nonce missing or too short\");\nif (!signatureRedisDAO.setNonce(appId, nonce, signature.timeout()*2, signature.timeUnit()))\n    throw new ServiceException(REPEATED_REQUESTS);","typeGuard":"static boolean isValidNonce(String n) { return n != null && n.length() >= 10; }","tryCatchPattern":"try { aspect.beforePointCut(joinPoint, signature); }\ncatch (ServiceException e) { if (e.getCode()==900 && \"存在重复请求\".equals(e.getMessage())) return ResponseEntity.status(409).body(\"replay\"); throw e; }","preventionTips":["Generate a fresh random nonce per request","Never reuse nonces across retries","Increase @ApiSignature timeout if latency is high"],"tags":["yudao","api-signature","anti-replay","nonce"],"backgroundTag":null,"analyzedSha":"0418084e222612af2fc1141f566af454f9236ab1","analyzedAt":"2026-08-14T00:56:18.412Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}