dromara/Sa-Token · error · NotLoginException

无效 token

Error message

无效 token

What it means

NotLoginException (type from StpUtil.TYPE, NotLoginException.INVALID_TOKEN) thrown by the SSE demo's SseEmitterHolder.createSse when StpUtil.getLoginIdByToken(satoken) returns null — i.e. the supplied token does not correspond to any active login session. Using the framework's NotLoginException (instead of a generic RuntimeException) lets sa-token's global exception handling render the standard not-logged-in response.

Solutions

  1. Log in first (StpUtil.login) and pass the exact token value via StpUtil.getTokenValue() when opening the SSE connection.
  2. If the token expired, re-authenticate and reconnect with the fresh token; consider sa-token auto-renew (active-timeout / renewal config) for long connections.
  3. Strip any 'Bearer ' prefix and whitespace from the satoken parameter before calling createSse.
  4. On the client, treat NotLoginException as a signal to redirect to login rather than retry-looping the SSE connect.

Example fix

// before
String satoken = request.getParameter("satoken"); // may contain "Bearer xxx"
SseEmitter emitter = SseEmitterHolder.createSse(satoken);

// after
String satoken = request.getParameter("satoken");
if (satoken != null && satoken.startsWith("Bearer ")) {
    satoken = satoken.substring(7);
}
if (StpUtil.getLoginIdByToken(satoken) == null) {
    return ResponseEntity.status(401).body("please login first");
}
SseEmitter emitter = SseEmitterHolder.createSse(satoken);
Defensive patterns

Strategy: validation

Validate before calling

String token = rawToken == null ? null : rawToken.replaceFirst("^Bearer ", "").trim();
if (token == null || StpUtil.getLoginIdByToken(token) == null) {
    return 401; // do not call SseEmitterHolder.createSse
}
SseEmitter emitter = SseEmitterHolder.createSse(token);

Type guard

boolean isValidToken(String t) { return t != null && StpUtil.getLoginIdByToken(t) != null; }

Try / catch

try { SseEmitterHolder.createSse(token); } catch (NotLoginException e) { // reply 401, client re-login then reconnect }

Prevention

When it happens

Trigger: Opening the SSE endpoint (/sse/connect?satoken=xxx) with a token that was never issued, has been logged out (StpUtil.logout invalidates it), kicked out, or expired per sa-token timeout config. Also when the token is passed with stray whitespace or the 'Bearer ' prefix still attached.

Common situations: Long-lived SSE connections outliving the token timeout (token expires while the client intends to reconnect); front-end storing a stale token after re-login; gateway stripping query parameters so satoken arrives empty.

Related errors


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

Appendix: source

Thrown at sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java:30

/**
 * SSE 连接管理器
 *
 * @author click33
 * @since 2025/4/11
 */
public class SseEmitterHolder {

    public static final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

    /**
     * 创建客户端
     */
    public static SseEmitter createSse(String satoken) {

        Object loginId = StpUtil.getLoginIdByToken(satoken);
        if(loginId == null) {
            throw new NotLoginException("无效 token", StpUtil.TYPE, NotLoginException.INVALID_TOKEN);
        }
        long uid = SaFoxUtil.getValueByType(loginId, Long.class);

        // 默认 30 秒超时,设置为 0L 则永不超时
        SseEmitter sseEmitter = new SseEmitter(600 * 1000L);
        sseEmitterMap.put(satoken, sseEmitter);
        System.out.println("连接成功:satoken=" + satoken + ",uid=" + uid);

        // 完成后回调
        sseEmitter.onCompletion(() -> {
            System.out.println("结束连接:satoken=" + satoken + ",uid=" + uid);
            sseEmitterMap.remove(satoken);
        });

        //超时回调
        sseEmitter.onTimeout(() -> {
            System.out.println("连接超时:satoken=" + satoken + ",uid=" + uid);
        });

View on GitHub (pinned to ac2c7f6e94)