{"record":{"id":"90d1d6a9eba7a18a","repo":"xkcoding/spring-boot-demo","slug":"5003-90d1d6","errorCode":"5003","errorMessage":"当前用户已在别处登录，请尝试更改密码或重新登录！","messagePattern":"当前用户已在别处登录，请尝试更改密码或重新登录！","errorType":"exception","errorClass":"SecurityException","httpStatus":null,"severity":"error","filePath":"demo-rbac-security/README.md","lineNumber":233,"sourceCode":"            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);\n        } catch (SignatureException e) {\n            log.error(\"无效的 Token 签名\");\n            throw new SecurityException(Status.TOKEN_PARSE_ERROR);\n        } catch (IllegalArgumentException e) {\n            log.error(\"Token 参数不存在\");\n            throw new SecurityException(Status.TOKEN_PARSE_ERROR);\n        }","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/xkcoding/spring-boot-demo/blob/87a142f9604c1a5365b4d24d22c2c11c26a9d5ab/demo-rbac-security/README.md#L215-L251","documentation":"parseJWT compares the presented jwt to the one stored in Redis under REDIS_JWT_KEY_PREFIX+username; a mismatch throws SecurityException(Status.TOKEN_OUT_OF_CTRL, code 5003) at README:233 (impl JwtUtil.java:103-104). This enforces single-session-per-account: a newer login overwrote the stored token, or the user logged out.","triggerScenarios":"The user logs in on a second device and createJWT overwrites the Redis key, invalidating the first device's token; or invalidateJWT deleted/changed the stored value.","commonSituations":"A single-session-per-account policy; logging out on one device while actively using another; testing the same account in two browsers.","solutions":["Inform the user another session took over and prompt re-login or password change","If concurrent sessions are desired, store a list of valid tokens per user instead of one","Track device/client info so the message can name the offending session"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"String stored = stringRedisTemplate.opsForValue().get(Consts.REDIS_JWT_KEY_PREFIX + username);\nif (!StrUtil.equals(token, stored)) {\n    // another session took over - prompt re-login before the aspect throws\n}","typeGuard":null,"tryCatchPattern":"try { jwtUtil.parseJWT(token); }\ncatch (SecurityException e) { if (Integer.valueOf(5003).equals(e.getCode())) { /* TOKEN_OUT_OF_CTRL - logged in elsewhere */ } }","preventionTips":["If concurrent sessions are allowed, store a set of tokens per user","Communicate the single-session policy clearly to users","Differentiate this 5003 path from the 5002 expiry path in the UI"],"tags":["jwt","session","single-session","auth"],"backgroundTag":null,"analyzedSha":"87a142f9604c1a5365b4d24d22c2c11c26a9d5ab","analyzedAt":"2026-08-14T01:16:58.217Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}