加群联系作者vx:xiaoda0423
仓库地址:https://webvueblog.github.io/JavaPlusDoc/
能力 | 说明 |
---|---|
支持 Header 传 token | 不光从 URL,也能从 WebSocket 请求头拿 token |
握手失败自定义返回码 | 拒绝连接时能带上明确的 HTTP 错误码,比如 401 未授权 |
支持主动踢人下线 | 比如token过期、恶意连接,可以主动断开 |
统一异常处理 | 保证连接失败时有标准返回,不是莫名其妙报错 |
🛠️ 进阶版完整代码
(1)改造 BaseEndpointConfigure
--- 高级版
go
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import javax.servlet.http.HttpSession;
import java.util.List;
import java.util.Map;
public class BaseEndpointConfigure extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
String token = extractToken(request);
if (!validateToken(token)) {
// 拒绝握手,抛一个特殊异常
throw new WebSocketAuthenticationException("Token无效,拒绝连接");
}
// 保存到用户属性,后续可以从Session里拿
sec.getUserProperties().put("token", token);
}
private String extractToken(HandshakeRequest request) {
// 1. 优先从 Header 中取
Map<String, List<String>> headers = request.getHeaders();
List<String> tokenList = headers.get("Sec-WebSocket-Protocol");
if (tokenList != null && !tokenList.isEmpty()) {
return tokenList.get(0);
}
// 2. 如果Header没有,从 URL query 参数取
String query = request.getQueryString();
if (query != null && query.startsWith("token=")) {
return query.substring(6);
}
return null;
}
private boolean validateToken(String token) {
// TODO: 这里可以接你的认证中心、JWT解析、Redis等逻辑
return token != null && token.length() > 5;
}
}
(2)定义一个「握手鉴权异常」WebSocketAuthenticationException
go
public class WebSocketAuthenticationException extends RuntimeException {
public WebSocketAuthenticationException(String message) {
super(message);
}
}
(3)在 WebSocketConfig
里添加统一异常处理
在 serverEndpointExporter()
方法外包装一层 try-catch。
go
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
@Bean
public BaseEndpointConfigure newConfigure() {
return new BaseEndpointConfigure();
}
}
(4)改造 @ServerEndpoint
--- 带token、踢人
go
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
@ServerEndpoint(value = "/ws/echo", configurator = BaseEndpointConfigure.class)
public class EchoWebSocket {
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
String token = (String) config.getUserProperties().get("token");
System.out.println("WebSocket连接建立,token=" + token);
// 你可以在这里根据业务逻辑判断
// 比如token过期了,踢掉
if ("expiredToken".equals(token)) {
try {
session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "Token已过期,断开连接"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
@OnMessage
public void onMessage(String message, Session session) {
try {
session.getBasicRemote().sendText("服务端返回:" + message);
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
System.out.println("WebSocket连接关闭");
}
@OnError
public void onError(Session session, Throwable error) {
System.err.println("WebSocket发生错误:" + error.getMessage());
}
}
🧠 核心变化总结
项目 | 变化点 |
---|---|
token提取 | 支持 Header 优先,URL 兜底 |
校验逻辑 | 灵活扩展,可以接认证中心、数据库 |
握手失败 | 自定义异常,友好提示 |
连接后控制 | 支持业务主动踢人 |
统一处理 | 保证握手和运行阶段都能优雅处理异常 |
📈 提升体验Tips(可选项)
-
如果想让客户端更友好收到拒绝原因,可以自定义 WebSocket握手拦截器 → 返回HTTP状态码,比如401
-
可以加上心跳检测机制,比如客户端定时发送ping消息,服务端回复pong
-
可以加SessionManager,统一管理所有在线Session(方便批量踢人/广播消息)
-
连接时支持 token
-
握手失败给明确的拒绝理由
-
连接成功后可以踢人
-
全程异常不留死角
-
可无缝对接后端认证体系
🎯 总体目标
-
每个登录用户,只能有一个有效WebSocket连接
-
新连接建立时,踢掉老连接(比如账号在别的地方登录)
-
有一个统一Session管理器,支持:
-
-
根据
userId
查找Session -
踢人(主动断开连接)
-
广播消息(选配)
-
-
接入现有的 token鉴权流程,自然兼容!
🏗️ 正式搭建
(1)新增统一管理类:WebSocketSessionManager
go
import javax.websocket.Session;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
public class WebSocketSessionManager {
// userId -> Session
private static final ConcurrentHashMap<String, Session> userSessionMap = new ConcurrentHashMap<>();
/**
* 添加新的连接
*/
public static void addSession(String userId, Session session) {
// 如果有旧连接,先踢掉
Session oldSession = userSessionMap.put(userId, session);
if (oldSession != null && oldSession.isOpen()) {
try {
oldSession.close(); // 踢掉老连接
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 移除连接
*/
public static void removeSession(String userId) {
userSessionMap.remove(userId);
}
/**
* 通过userId获取Session
*/
public static Session getSession(String userId) {
return userSessionMap.get(userId);
}
/**
* 主动发送消息
*/
public static void sendMessage(String userId, String message) {
Session session = getSession(userId);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 踢掉某个用户
*/
public static void kickOutUser(String userId, String reason) {
Session session = getSession(userId);
if (session != null && session.isOpen()) {
try {
session.close(new javax.websocket.CloseReason(
javax.websocket.CloseReason.CloseCodes.NORMAL_CLOSURE, reason
));
} catch (IOException e) {
e.printStackTrace();
}
removeSession(userId);
}
}
}
(2)在 @ServerEndpoint
里接入管理器
比如在 EchoWebSocket
里用:
go
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
@ServerEndpoint(value = "/ws/echo", configurator = BaseEndpointConfigure.class)
public class EchoWebSocket {
private String userId;
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
String token = (String) config.getUserProperties().get("token");
// 假设这里解析出来userId(一般token里面带的)
this.userId = parseUserIdFromToken(token);
System.out.println("WebSocket连接建立,userId=" + userId);
// 加入Session管理器
WebSocketSessionManager.addSession(userId, session);
}
@OnMessage
public void onMessage(String message, Session session) {
try {
session.getBasicRemote().sendText("服务端返回:" + message);
} catch (Exception e) {
e.printStackTrace();
}
}
@OnClose
public void onClose(Session session) {
System.out.println("WebSocket连接关闭,userId=" + userId);
WebSocketSessionManager.removeSession(userId);
}
@OnError
public void onError(Session session, Throwable error) {
System.err.println("WebSocket发生错误:" + error.getMessage());
}
private String parseUserIdFromToken(String token) {
// TODO: 实际可以解析JWT或者查Redis拿到userId
return token; // 这里简单演示,假设token本身就是userId
}
}
✅ 这样每次有新连接:
-
自动把老的连接踢掉
-
新的连接注册进管理器
-
关闭时/出错时也自动清理Session
(3)其他地方想踢人怎么办?
比如后台管理系统发现用户异常,需要踢人:
go
WebSocketSessionManager.kickOutUser("userId123", "你的账号在别处登录,已被强制下线");
一行代码,干掉!
📈 整体流程图
go
客户端连接 -> 解析token -> 解析userId -> WebSocketSessionManager管理session
↓
同userId已有session?
↓是
踢掉老session,注册新session
-
userSessionMap
也可以换成Redisson
的RMap
,支持分布式节点同步踢人! -
可以在服务端定时心跳检测,把死掉的连接清理掉
-
可以扩展消息总线,做到一条命令踢掉多个服务节点的连接
能力 | 说明 |
---|---|
✅ | 在线用户统一管理 |
✅ | 支持单点登录强制踢掉旧连接 |
✅ | 异常情况自动清理Session |
✅ | 跨服务节点可以扩展Redisson同步 |
📦 WebSocket心跳机制 + 服务端检测离线清理 + 自动重连 + 分布式节点同步踢人