dromara/Sa-Token · error · SaJwtException

30204

30204

Error message

jwt 已过期:

What it means

Thrown by SaJwtTemplate.parseToken when isCheckTimeout is true and the token's EFF (effective-until) claim is null or earlier than the current time. Code 30204 marks an expired JWT; NEVER_EXPIRE tokens skip this check.

Source

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

    	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);
        		}
        	}
    	}
    	
        // 返回 
        return jwt;
    }

    /**
     * 获取 jwt 数据载荷 (校验 sign、loginType、timeout) 
     * @param token token值
     * @param loginType 登录类型 
     * @param keyt 秘钥 
     * @return 载荷 
     */
    public JSONObject getPayloads(String token, String loginType, String keyt) {
    	return parseToken(token, loginType, keyt, true).getPayloads();
    }

View on GitHub (pinned to ac2c7f6e94)

Solutions

  1. Have the client refresh/re-login to obtain a new token when 30204 is returned
  2. Verify server clocks (NTP) on verifying nodes — clock drift directly affects this check
  3. If you forge tokens yourself, ensure the EFF claim uses milliseconds and a future timestamp, or SaTokenDao.NEVER_EXPIRE

Example fix

// before
// client keeps a 1h token for days
parseToken(staleToken, "login", secret, true); // 30204

// after
// client: on 30204, obtain a fresh token and retry
try {
    parseToken(token, "login", secret, true);
} catch (SaJwtException e) {
    if (isCode(e, SaJwtErrorCode.CODE_30204)) {
        token = refreshLogin(); // re-login / refresh endpoint
        parseToken(token, "login", secret, true);
    } else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

JWT jwt = JWT.of(token);
Long eff = jwt.getPayloads().getLong("eff", 0L);
boolean expiringSoon = eff != -1 && eff < System.currentTimeMillis() + 60_000L;
if (expiringSoon) token = refreshToken();

Try / catch

catch (SaJwtException e) {
    if (e.getCode() == SaJwtErrorCode.CODE_30204) {
        token = refreshLogin();
        return retryOnce(request); // single retry, then fail
    }
    throw e;
}

Prevention

When it happens

Trigger: Parsing a JWT past its embedded expiry while timeout checking is enabled (most read paths enable it). The eff claim is compared against System.currentTimeMillis().

Common situations: Client keeps using a token after its lifetime elapsed (JWTs are self-contained, no server-side renewal); app-server clock skew making valid tokens appear expired; token issued with a very short eff during testing; mixing second- and millisecond-scale timestamps when creating tokens manually.

Related errors


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