{"record":{"id":"acd9314029f526a8","repo":"dromara/Sa-Token","slug":"token-satoken","errorCode":null,"errorMessage":"连接失败，无效Token：${satoken}","messagePattern":"连接失败，无效Token：(.+?)","errorType":"validation","errorClass":"SaTokenException","httpStatus":null,"severity":"error","filePath":"sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java","lineNumber":45,"sourceCode":"    /**\n     * 固定前缀 \n     */\n    private static final String USER_ID = \"user_id_\";\n\t\n\t /** \n\t  * 存放Session集合，方便推送消息 （javax.websocket.Session）  \n\t  */\n    private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();\n    \n\t// 监听：连接成功\n\t@OnOpen\n\tpublic void onOpen(Session session, @PathParam(\"satoken\") String satoken) throws IOException {\n\t\t\n\t\t// 根据 token 获取对应的 userId \n\t\tObject loginId = StpUtil.getLoginIdByToken(satoken);\n\t\tif(loginId == null) {\n\t\t\tsession.close();\n\t\t\tthrow new SaTokenException(\"连接失败，无效Token：\" + satoken);\n\t\t}\n\t\t\n\t\t// put到集合，方便后续操作 \n\t\tlong userId = SaFoxUtil.getValueByType(loginId, long.class);\n\t\tsessionMap.put(USER_ID + userId, session);\n\t\t\n\t\t// 给个提示 \n\t\tString tips = \"Web-Socket 连接成功，sid=\" + session.getId() + \"，userId=\" + userId;\n\t\tSystem.out.println(tips);\n\t\tsendMessage(session, tips);\n\t}\n\n\t// 监听: 连接关闭\n\t@OnClose\n\tpublic void onClose(Session session) {\n\t\tSystem.out.println(\"连接关闭，sid=\" + session.getId());\n\t\tfor (String key : sessionMap.keySet()) {\n\t\t\tif(sessionMap.get(key).getId().equals(session.getId())) {","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/dromara/Sa-Token/blob/ac2c7f6e94a78573cf0bcb932dd8b04e68fad189/sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java#L27-L63","documentation":"SaTokenException thrown in the WebSocket demo's @OnOpen handler when StpUtil.getLoginIdByToken(satoken) returns null — the path-provided token is invalid — after the session has already been closed(). Because it fires inside the WebSocket handshake/open callback, the exception surfaces in the container log (or as a handshake error) rather than as a normal HTTP error response.","triggerScenarios":"Connecting to ws://host/ws/satoken=xxx (path param) where xxx is an expired, logged-out, or never-issued token. Note the token travels as a @PathParam, so URL-encoding issues, a missing path segment, or the 'Bearer ' prefix also produce a token string that yields no loginId.","commonSituations":"Front-end opens the WebSocket before login completes; token refreshed by HTTP calls but the WS URL still holds the old token; proxies rewriting the URL path so the satoken segment is lost or double-decoded.","solutions":["Log in first and build the WS URL with the current token: new WebSocket(\"ws://host/ws/\" + StpUtil.getTokenValue()).","If the token can expire, reconnect logic should re-fetch a valid token on 401/handshake failure before retrying.","Verify the path template matches (@PathParam(\"satoken\") vs the server endpoint pattern) so the token is not null/empty.","Strip 'Bearer ' and encode the token if the front-end stores it with a prefix."],"exampleFix":"// before (js)\nconst ws = new WebSocket(\"ws://localhost:8081/ws/satoken=\" + staleToken);\n\n// after\nconst token = localStorage.getItem(\"satoken\").replace(/^Bearer /, \"\");\nconst ws = new WebSocket(\"ws://localhost:8081/ws/\" + token);","handlingStrategy":"validation","validationCode":"String token = raw.replaceFirst(\"^Bearer \", \"\").trim();\nif (StpUtil.getLoginIdByToken(token) == null) {\n    // refuse before handshake; do not attempt the WS connection\n    return 401;\n}\nnew WebSocket(\"ws://host/ws/\" + token);","typeGuard":"boolean tokenValid(String t) { return t != null && !t.isEmpty() && StpUtil.getLoginIdByToken(t) != null; }","tryCatchPattern":"try { webSocket.onOpen(...); } catch (SaTokenException e) { if (e.getMessage().startsWith(\"连接失败，无效Token\")) { /* fetch fresh token and reconnect once */ } }","preventionTips":["Build the WS URL from StpUtil.getTokenValue() at connect time, not from a token cached at page load.","Verify the endpoint path template includes the {satoken} path param exactly as @PathParam declares.","Implement one-shot reconnect-with-fresh-token on handshake failure instead of blind retries."],"tags":["websocket","token-invalid","onopen","sa-token"],"backgroundTag":null,"analyzedSha":"ac2c7f6e94a78573cf0bcb932dd8b04e68fad189","analyzedAt":"2026-08-14T14:36:10.271Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}