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
- Have the client refresh/re-login to obtain a new token when 30204 is returned
- Verify server clocks (NTP) on verifying nodes — clock drift directly affects this check
- 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
- Sync verifying servers with NTP — clock skew causes false 30204
- Issue short-lived tokens plus a refresh endpoint rather than long-lived JWTs
- Proactively refresh tokens near expiry instead of waiting for failure
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.