{"record":{"id":"1e2d3c959bb44b61","repo":"xkcoding/spring-boot-demo","slug":"5002-1e2d3c","errorCode":"5002","errorMessage":"token 已过期，请重新登录！","messagePattern":"token 已过期，请重新登录！","errorType":"exception","errorClass":"SecurityException","httpStatus":null,"severity":"error","filePath":"demo-rbac-security/README.md","lineNumber":226,"sourceCode":"     * 解析JWT\n     *\n     * @param jwt JWT\n     * @return {@link Claims}\n     */\n    public Claims parseJWT(String jwt) {\n        try {\n            Claims claims = Jwts.parser()\n                    .setSigningKey(jwtConfig.getKey())\n                    .parseClaimsJws(jwt)\n                    .getBody();\n\n            String username = claims.getSubject();\n            String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username;\n\n            // 校验redis中的JWT是否存在\n            Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);\n            if (Objects.isNull(expire) || expire <= 0) {\n                throw new SecurityException(Status.TOKEN_EXPIRED);\n            }\n\n            // 校验redis中的JWT是否与当前的一致，不一致则代表用户已注销/用户在不同设备登录，均代表JWT已过期\n            String redisToken = stringRedisTemplate.opsForValue()\n                    .get(redisKey);\n            if (!StrUtil.equals(jwt, redisToken)) {\n                throw new SecurityException(Status.TOKEN_OUT_OF_CTRL);\n            }\n            return claims;\n        } catch (ExpiredJwtException e) {\n            log.error(\"Token 已过期\");\n            throw new SecurityException(Status.TOKEN_EXPIRED);\n        } catch (UnsupportedJwtException e) {\n            log.error(\"不支持的 Token\");\n            throw new SecurityException(Status.TOKEN_PARSE_ERROR);\n        } catch (MalformedJwtException e) {\n            log.error(\"Token 无效\");\n            throw new SecurityException(Status.TOKEN_PARSE_ERROR);","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/xkcoding/spring-boot-demo/blob/87a142f9604c1a5365b4d24d22c2c11c26a9d5ab/demo-rbac-security/README.md#L208-L244","documentation":"parseJWT checks the Redis TTL of key Consts.REDIS_JWT_KEY_PREFIX+username; if stringRedisTemplate.getExpire returns null or <= 0 it throws SecurityException(Status.TOKEN_EXPIRED) (README:226; impl JwtUtil.java:97-98). This is the REDIS-side expiry, separate from the JWT exp claim - the token may still be cryptographically valid but is treated as expired because Redis no longer holds it.","triggerScenarios":"Redis evicted or expired the JWT key (its ttl elapsed) before the JWT exp claim expired; Redis was flushed or restarted without persistence; invalidateJWT deleted the key on logout.","commonSituations":"Redis restart without AOF/RDB; a manual FLUSHDB; the Redis ttl set shorter than the JWT exp ttl; logout performed on another device.","solutions":["Enable Redis persistence (AOF/RDB) if tokens must survive restarts","Prompt re-login to repopulate the Redis JWT key via createJWT","Keep the Redis ttl aligned with the JWT exp ttl set in createJWT","Avoid flushing the JWT keyspace in shared environments"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"Long expire = stringRedisTemplate.getExpire(Consts.REDIS_JWT_KEY_PREFIX + username, TimeUnit.MILLISECONDS);\nif (expire == null || expire <= 0) {\n    // Redis no longer holds the token - force re-login before trusting the JWT\n}","typeGuard":null,"tryCatchPattern":"try { jwtUtil.parseJWT(token); }\ncatch (SecurityException e) { if (Status.TOKEN_EXPIRED.getCode().equals(e.getCode())) { /* Redis TTL expired - re-login */ } }","preventionTips":["Enable Redis persistence so JWT keys survive restarts","Keep the Redis ttl equal to the JWT exp ttl","Never FLUSHDB on a Redis instance holding live sessions"],"tags":["jwt","redis","expired-token","session"],"backgroundTag":null,"analyzedSha":"87a142f9604c1a5365b4d24d22c2c11c26a9d5ab","analyzedAt":"2026-08-14T01:16:58.217Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}