xkcoding/spring-boot-demo · error · SecurityException

5003

5003

Error message

当前用户已在别处登录,请尝试更改密码或重新登录!

What it means

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.

Source

Thrown at demo-rbac-security/README.md:233

            Claims claims = Jwts.parser()
                    .setSigningKey(jwtConfig.getKey())
                    .parseClaimsJws(jwt)
                    .getBody();

            String username = claims.getSubject();
            String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username;

            // 校验redis中的JWT是否存在
            Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);
            if (Objects.isNull(expire) || expire <= 0) {
                throw new SecurityException(Status.TOKEN_EXPIRED);
            }

            // 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期
            String redisToken = stringRedisTemplate.opsForValue()
                    .get(redisKey);
            if (!StrUtil.equals(jwt, redisToken)) {
                throw new SecurityException(Status.TOKEN_OUT_OF_CTRL);
            }
            return claims;
        } catch (ExpiredJwtException e) {
            log.error("Token 已过期");
            throw new SecurityException(Status.TOKEN_EXPIRED);
        } catch (UnsupportedJwtException e) {
            log.error("不支持的 Token");
            throw new SecurityException(Status.TOKEN_PARSE_ERROR);
        } catch (MalformedJwtException e) {
            log.error("Token 无效");
            throw new SecurityException(Status.TOKEN_PARSE_ERROR);
        } catch (SignatureException e) {
            log.error("无效的 Token 签名");
            throw new SecurityException(Status.TOKEN_PARSE_ERROR);
        } catch (IllegalArgumentException e) {
            log.error("Token 参数不存在");
            throw new SecurityException(Status.TOKEN_PARSE_ERROR);
        }

View on GitHub (pinned to 87a142f960)

Solutions

  1. Inform the user another session took over and prompt re-login or password change
  2. If concurrent sessions are desired, store a list of valid tokens per user instead of one
  3. Track device/client info so the message can name the offending session
Defensive patterns

Strategy: try-catch

Validate before calling

String stored = stringRedisTemplate.opsForValue().get(Consts.REDIS_JWT_KEY_PREFIX + username);
if (!StrUtil.equals(token, stored)) {
    // another session took over - prompt re-login before the aspect throws
}

Try / catch

try { jwtUtil.parseJWT(token); }
catch (SecurityException e) { if (Integer.valueOf(5003).equals(e.getCode())) { /* TOKEN_OUT_OF_CTRL - logged in elsewhere */ } }

Prevention

When it happens

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

Common situations: A single-session-per-account policy; logging out on one device while actively using another; testing the same account in two browsers.

Related errors


AI-assisted analysis of xkcoding/spring-boot-demo@87a142f960 (2026-08-14). Data as JSON: /api/errors/90d1d6a9eba7a18a. Report an issue: GitHub.