{"record":{"id":"93b18322843e15a9","repo":"dromara/Sa-Token","slug":"nonce-nonce","errorCode":null,"errorMessage":"此 nonce 已被使用过，不可重复使用：{nonce}","messagePattern":"此 nonce 已被使用过，不可重复使用：(.+?)","errorType":"exception","errorClass":"SaSignException","httpStatus":null,"severity":"error","filePath":"sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignTemplate.java","lineNumber":276,"sourceCode":"\t\tString key = splicingNonceSaveKey(nonce);\n\t\treturn SaManager.getSaTokenDao().get(key) == null;\n\t}\n\n\t/**\n\t * 校验：随机字符串 nonce 是否有效，如果无效则抛出异常。\n\t * \t\t注意：同一 nonce 只可以被校验通过一次，校验后将保存在缓存中，再次校验将无法通过\n\t * @param nonce 待校验的随机字符串\n\t */\n\tpublic void checkNonce(String nonce) {\n\t\t// 为空代表无效\n\t\tif(SaFoxUtil.isEmpty(nonce)) {\n\t\t\tthrow new SaSignException(\"nonce 为空，无效\");\n\t\t}\n\n\t\t// 校验此 nonce 是否已被使用过\n\t\tString key = splicingNonceSaveKey(nonce);\n\t\tif(SaManager.getSaTokenDao().get(key) != null) {\n\t\t\tthrow new SaSignException(\"此 nonce 已被使用过，不可重复使用：\" + nonce);\n\t\t}\n\n\t\t// 校验通过后，将此 nonce 保存在缓存中，保证下次校验无法通过\n\t\tSaManager.getSaTokenDao().set(key, nonce, getSignConfigOrGlobal().getSaveNonceExpire() * 2 + 2);\n\t}\n\n\t/**\n\t * 判断：给定的参数 生成的签名是否为有效签名\n\t * @param paramsMap 参数列表\n\t * @param sign 待验证的签名\n\t * @return 签名是否有效\n\t */\n\tpublic boolean isValidSign(Map<String, ?> paramsMap, String sign) {\n\t\tString theSign = createSign(paramsMap);\n\t\treturn theSign.equals(sign);\n\t}\n\n\t/**","sourceCodeStart":258,"sourceCodeEnd":294,"githubUrl":"https://github.com/dromara/Sa-Token/blob/ac2c7f6e94a78573cf0bcb932dd8b04e68fad189/sa-token-plugin/sa-token-sign/src/main/java/cn/dev33/satoken/sign/template/SaSignTemplate.java#L258-L294","documentation":"Thrown by SaSignTemplate.checkNonce when the nonce's cache key already exists in the DAO, i.e. this exact nonce value passed a check before and is still within its save window (saveNonceExpire * 2 + 2 seconds). This enforces single-use nonces so an intercepted signed request cannot be replayed verbatim.","triggerScenarios":"Sending the identical signed request twice — network retry, client-side duplicate submit, or an attacker replaying a captured request — within the nonce retention window.","commonSituations":"HTTP client automatic retries on timeout; user double-clicks submit; idempotent background jobs reusing a cached signed request; legitimate retry logic that signs once and sends many times.","solutions":["Re-sign each request attempt with a fresh nonce and timestamp instead of replaying the same signed string","Disable or debounce duplicate submissions on the client (button disable, request dedup)","If retries must be identical, make the operation idempotent server-side and accept that the duplicate signed call is rejected"],"exampleFix":"// before\nString signedQuery = buildSignedQueryOnce(params); // reused for every retry\nhttp.post(signedQuery);\n// after\nhttp.post(buildSignedQuery(params)); // re-signs: new nonce + timestamp each attempt","handlingStrategy":"retry","validationCode":"String key = \"satoken:sign:nonce:\" + nonce;\nif (SaManager.getSaTokenDao().get(key) != null) throw new IllegalStateException(\"nonce already used — re-sign with a new nonce\");","typeGuard":null,"tryCatchPattern":"try { saSignTemplate.checkNonce(nonce); } catch (SaSignException e) { if (e.getMessage().contains(\"已被使用过\")) { reSignWithFreshNonceAndTimestamp(); retry(request); } else throw e; }","preventionTips":["Re-sign with a new nonce and timestamp on every retry attempt","Disable automatic retries at the HTTP-client level for signed calls, or hook retry to re-sign"],"tags":["api-sign","nonce","replay-protection","retry"],"backgroundTag":null,"analyzedSha":"ac2c7f6e94a78573cf0bcb932dd8b04e68fad189","analyzedAt":"2026-08-14T14:36:10.271Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}