{"record":{"id":"3010911bd43380da","repo":"dromara/Sa-Token","slug":"30202","errorCode":"30202","errorMessage":"jwt 签名无效：","messagePattern":"jwt 签名无效：","errorType":"exception","errorClass":"SaJwtException","httpStatus":null,"severity":"critical","filePath":"sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java","lineNumber":193,"sourceCode":"\n    \t// 如果token为null \n    \tif(token == null) {\n    \t\tthrow new SaJwtException(\"jwt 字符串不可为空\");\n    \t}\n    \t\n    \t// 解析 \n    \tJWT jwt;\n    \ttry {\n    \t\tjwt = JWT.of(token);\n\t\t} catch (JWTException | JSONException e) {\n    \t\tthrow new SaJwtException(\"jwt 解析失败：\" + token, e).setCode(SaJwtErrorCode.CODE_30201);\n\t\t}\n    \tJSONObject payloads = jwt.getPayloads();\n    \t\n    \t// 校验 Token 签名\n\t\tboolean verify = jwt.setSigner(createSigner(keyt)).verify();\n    \tif( ! verify) {\n    \t\tthrow new SaJwtException(\"jwt 签名无效：\" + token).setCode(SaJwtErrorCode.CODE_30202);\n    \t}\n\n    \t// 校验 loginType \n    \tif( ! Objects.equals(loginType, payloads.getStr(LOGIN_TYPE))) {\n    \t\tthrow new SaJwtException(\"jwt loginType 无效：\" + token).setCode(SaJwtErrorCode.CODE_30203);\n    \t}\n    \t\n    \t// 校验 Token 有效期\n    \tif(isCheckTimeout) {\n    \t\tLong effTime = payloads.getLong(EFF, 0L);\n        \tif(effTime != NEVER_EXPIRE) {\n        \t\tif(effTime == null || effTime < System.currentTimeMillis()) {\n        \t\t\tthrow new SaJwtException(\"jwt 已过期：\" + token).setCode(SaJwtErrorCode.CODE_30204);\n        \t\t}\n        \t}\n    \t}\n    \t\n        // 返回 ","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/dromara/Sa-Token/blob/ac2c7f6e94a78573cf0bcb932dd8b04e68fad189/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java#L175-L211","documentation":"Thrown by SaJwtTemplate.parseToken when jwt.setSigner(createSigner(keyt)).verify() returns false: the token parses fine but its signature does not match the configured secret. Code 30202 marks a signature verification failure — the token was signed with a different key or tampered with.","triggerScenarios":"Parsing a token with a jwt-secret-key different from the one used to sign it; also triggered by algorithm-keytype mismatch (e.g. token signed RS256 but secret configured for HMAC-style verification).","commonSituations":"Secret rotated or differs between the issuing service and the verifying service; different jwt-secret-key per environment (dev token sent to prod); typo in the secret; secret env var unset so a default/fallback value is used; using asymmetric keys without configuring key-type properly.","solutions":["Ensure the verifying service uses exactly the same jwt-secret-key (and key-type/algorithm settings) as the issuing service","If the secret was rotated, re-issue tokens or keep old tokens valid via a grace period with the previous secret","Check for environment drift: print the config source / compare env vars on both sides (name only, never the value)"],"exampleFix":"# before\n# issuer: jwt-secret-key: ${JWT_SECRET_ISSUE}\n# verifier: jwt-secret-key: ${JWT_SECRET}   # different value -> 30202\n\n# after\n# both services read the same variable\nsa-token:\n  jwt-secret-key: ${JWT_SECRET}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"catch (SaJwtException e) {\n    if (e.getCode() == SaJwtErrorCode.CODE_30202) {\n        // 401: reject token; possible tampering or secret drift — alert if unexpected\n    }\n}","preventionTips":["Share the jwt secret via one secret store across services; never duplicate literals","When rotating secrets, overlap old/new acceptance windows or force re-issue","Treat unexpected 30202 spikes as a security incident"],"tags":["sa-token","jwt","signature","security","configuration"],"backgroundTag":null,"analyzedSha":"ac2c7f6e94a78573cf0bcb932dd8b04e68fad189","analyzedAt":"2026-08-14T14:36:10.271Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}