{"record":{"id":"a3852d04adf3a440","repo":"dromara/Sa-Token","slug":"token","errorCode":null,"errorMessage":"无效 token","messagePattern":"无效 token","errorType":"validation","errorClass":"NotLoginException","httpStatus":null,"severity":"error","filePath":"sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java","lineNumber":30,"sourceCode":"\n/**\n * SSE 连接管理器\n *\n * @author click33\n * @since 2025/4/11\n */\npublic class SseEmitterHolder {\n\n    public static final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();\n\n    /**\n     * 创建客户端\n     */\n    public static SseEmitter createSse(String satoken) {\n\n        Object loginId = StpUtil.getLoginIdByToken(satoken);\n        if(loginId == null) {\n            throw new NotLoginException(\"无效 token\", StpUtil.TYPE, NotLoginException.INVALID_TOKEN);\n        }\n        long uid = SaFoxUtil.getValueByType(loginId, Long.class);\n\n        // 默认 30 秒超时，设置为 0L 则永不超时\n        SseEmitter sseEmitter = new SseEmitter(600 * 1000L);\n        sseEmitterMap.put(satoken, sseEmitter);\n        System.out.println(\"连接成功：satoken=\" + satoken + \"，uid=\" + uid);\n\n        // 完成后回调\n        sseEmitter.onCompletion(() -> {\n            System.out.println(\"结束连接：satoken=\" + satoken + \"，uid=\" + uid);\n            sseEmitterMap.remove(satoken);\n        });\n\n        //超时回调\n        sseEmitter.onTimeout(() -> {\n            System.out.println(\"连接超时：satoken=\" + satoken + \"，uid=\" + uid);\n        });","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/dromara/Sa-Token/blob/ac2c7f6e94a78573cf0bcb932dd8b04e68fad189/sa-token-demo/sa-token-demo-sse/src/main/java/com/pj/util/SseEmitterHolder.java#L12-L48","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Log in first (StpUtil.login) and pass the exact token value via StpUtil.getTokenValue() when opening the SSE connection.","If the token expired, re-authenticate and reconnect with the fresh token; consider sa-token auto-renew (active-timeout / renewal config) for long connections.","Strip any 'Bearer ' prefix and whitespace from the satoken parameter before calling createSse.","On the client, treat NotLoginException as a signal to redirect to login rather than retry-looping the SSE connect."],"exampleFix":"// before\nString satoken = request.getParameter(\"satoken\"); // may contain \"Bearer xxx\"\nSseEmitter emitter = SseEmitterHolder.createSse(satoken);\n\n// after\nString satoken = request.getParameter(\"satoken\");\nif (satoken != null && satoken.startsWith(\"Bearer \")) {\n    satoken = satoken.substring(7);\n}\nif (StpUtil.getLoginIdByToken(satoken) == null) {\n    return ResponseEntity.status(401).body(\"please login first\");\n}\nSseEmitter emitter = SseEmitterHolder.createSse(satoken);","handlingStrategy":"validation","validationCode":"String token = rawToken == null ? null : rawToken.replaceFirst(\"^Bearer \", \"\").trim();\nif (token == null || StpUtil.getLoginIdByToken(token) == null) {\n    return 401; // do not call SseEmitterHolder.createSse\n}\nSseEmitter emitter = SseEmitterHolder.createSse(token);","typeGuard":"boolean isValidToken(String t) { return t != null && StpUtil.getLoginIdByToken(t) != null; }","tryCatchPattern":"try { SseEmitterHolder.createSse(token); } catch (NotLoginException e) { // reply 401, client re-login then reconnect }","preventionTips":["Open SSE connections only after a successful login, using StpUtil.getTokenValue() for the current token.","On the browser side, treat NotLoginException responses as a re-login signal, not a retry trigger.","Strip 'Bearer ' prefixes before passing the token as a query parameter."],"tags":["sse","not-login","token-invalid","sa-token"],"backgroundTag":null,"analyzedSha":"ac2c7f6e94a78573cf0bcb932dd8b04e68fad189","analyzedAt":"2026-08-14T14:36:10.271Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}