dromara/Sa-Token · error · SaTokenException
连接失败,无效Token:${satoken}
Error message
连接失败,无效Token:${satoken} What it means
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.
Source
Thrown at sa-token-demo/sa-token-demo-websocket/src/main/java/com/pj/ws/WebSocketConnect.java:45
/**
* 固定前缀
*/
private static final String USER_ID = "user_id_";
/**
* 存放Session集合,方便推送消息 (javax.websocket.Session)
*/
private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
// 监听:连接成功
@OnOpen
public void onOpen(Session session, @PathParam("satoken") String satoken) throws IOException {
// 根据 token 获取对应的 userId
Object loginId = StpUtil.getLoginIdByToken(satoken);
if(loginId == null) {
session.close();
throw new SaTokenException("连接失败,无效Token:" + satoken);
}
// put到集合,方便后续操作
long userId = SaFoxUtil.getValueByType(loginId, long.class);
sessionMap.put(USER_ID + userId, session);
// 给个提示
String tips = "Web-Socket 连接成功,sid=" + session.getId() + ",userId=" + userId;
System.out.println(tips);
sendMessage(session, tips);
}
// 监听: 连接关闭
@OnClose
public void onClose(Session session) {
System.out.println("连接关闭,sid=" + session.getId());
for (String key : sessionMap.keySet()) {
if(sessionMap.get(key).getId().equals(session.getId())) {View on GitHub (pinned to ac2c7f6e94)
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.
Example fix
// before (js)
const ws = new WebSocket("ws://localhost:8081/ws/satoken=" + staleToken);
// after
const token = localStorage.getItem("satoken").replace(/^Bearer /, "");
const ws = new WebSocket("ws://localhost:8081/ws/" + token); Defensive patterns
Strategy: validation
Validate before calling
String token = raw.replaceFirst("^Bearer ", "").trim();
if (StpUtil.getLoginIdByToken(token) == null) {
// refuse before handshake; do not attempt the WS connection
return 401;
}
new WebSocket("ws://host/ws/" + token); Type guard
boolean tokenValid(String t) { return t != null && !t.isEmpty() && StpUtil.getLoginIdByToken(t) != null; } Try / catch
try { webSocket.onOpen(...); } catch (SaTokenException e) { if (e.getMessage().startsWith("连接失败,无效Token")) { /* fetch fresh token and reconnect once */ } } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of dromara/Sa-Token@ac2c7f6e94 (2026-08-14).
Data as JSON: /api/errors/acd9314029f526a8.
Report an issue: GitHub.