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

  1. Log in first and build the WS URL with the current token: new WebSocket("ws://host/ws/" + StpUtil.getTokenValue()).
  2. If the token can expire, reconnect logic should re-fetch a valid token on 401/handshake failure before retrying.
  3. Verify the path template matches (@PathParam("satoken") vs the server endpoint pattern) so the token is not null/empty.
  4. 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

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.