dromara/Sa-Token · critical · SaJwtException

30202

30202

Error message

jwt 签名无效:

What it means

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.

Source

Thrown at sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java:193

    	// 如果token为null 
    	if(token == null) {
    		throw new SaJwtException("jwt 字符串不可为空");
    	}
    	
    	// 解析 
    	JWT jwt;
    	try {
    		jwt = JWT.of(token);
		} catch (JWTException | JSONException e) {
    		throw new SaJwtException("jwt 解析失败:" + token, e).setCode(SaJwtErrorCode.CODE_30201);
		}
    	JSONObject payloads = jwt.getPayloads();
    	
    	// 校验 Token 签名
		boolean verify = jwt.setSigner(createSigner(keyt)).verify();
    	if( ! verify) {
    		throw new SaJwtException("jwt 签名无效:" + token).setCode(SaJwtErrorCode.CODE_30202);
    	}

    	// 校验 loginType 
    	if( ! Objects.equals(loginType, payloads.getStr(LOGIN_TYPE))) {
    		throw new SaJwtException("jwt loginType 无效:" + token).setCode(SaJwtErrorCode.CODE_30203);
    	}
    	
    	// 校验 Token 有效期
    	if(isCheckTimeout) {
    		Long effTime = payloads.getLong(EFF, 0L);
        	if(effTime != NEVER_EXPIRE) {
        		if(effTime == null || effTime < System.currentTimeMillis()) {
        			throw new SaJwtException("jwt 已过期:" + token).setCode(SaJwtErrorCode.CODE_30204);
        		}
        	}
    	}
    	
        // 返回 

View on GitHub (pinned to ac2c7f6e94)

Solutions

  1. Ensure the verifying service uses exactly the same jwt-secret-key (and key-type/algorithm settings) as the issuing service
  2. If the secret was rotated, re-issue tokens or keep old tokens valid via a grace period with the previous secret
  3. Check for environment drift: print the config source / compare env vars on both sides (name only, never the value)

Example fix

# before
# issuer: jwt-secret-key: ${JWT_SECRET_ISSUE}
# verifier: jwt-secret-key: ${JWT_SECRET}   # different value -> 30202

# after
# both services read the same variable
sa-token:
  jwt-secret-key: ${JWT_SECRET}
Defensive patterns

Strategy: try-catch

Try / catch

catch (SaJwtException e) {
    if (e.getCode() == SaJwtErrorCode.CODE_30202) {
        // 401: reject token; possible tampering or secret drift — alert if unexpected
    }
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of dromara/Sa-Token@ac2c7f6e94 (2026-08-14). Data as JSON: /api/errors/3010911bd43380da. Report an issue: GitHub.