分布式微服务系统架构第120集:专业WebSocket鉴权

加群联系作者vx:xiaoda0423

仓库地址:https://webvueblog.github.io/JavaPlusDoc/

https://1024bat.cn/

能力 说明
支持 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 也可以换成 RedissonRMap,支持分布式节点同步踢人

  • 可以在服务端定时心跳检测,把死掉的连接清理掉

  • 可以扩展消息总线,做到一条命令踢掉多个服务节点的连接

能力 说明
在线用户统一管理
支持单点登录强制踢掉旧连接
异常情况自动清理Session
跨服务节点可以扩展Redisson同步

📦 WebSocket心跳机制 + 服务端检测离线清理 + 自动重连 + 分布式节点同步踢人

相关推荐
littleplayer4 分钟前
iOS Swift Redux 架构详解
前端·设计模式·架构
零一码场7 分钟前
IMA之ima_read_file 和 ima_post_read_file不同
架构
阿里云云原生1 小时前
API 即 MCP|Higress 发布 MCP Marketplace,加速存量 API 跨入 MCP 时代
云原生
bing_1581 小时前
Redis 的单线程模型对微服务意味着什么?需要注意哪些潜在瓶颈?
数据库·redis·微服务
掘金-我是哪吒2 小时前
分布式微服务系统架构第119集:WebSocket监控服务内部原理和执行流程
分布式·websocket·微服务·架构·系统架构
Leo.yuan4 小时前
数据仓库是什么?数据仓库架构有哪些?
大数据·数据库·数据仓库·架构·数据分析
柳如烟@4 小时前
CentOS 7上Memcached的安装、配置及高可用架构搭建
架构·centos·memcached
爱吃龙利鱼4 小时前
rocky9.4部署k8s群集v1.28.2版本(containerd)(纯命令)
云原生·容器·kubernetes
西洼工作室4 小时前
黑马商城-微服务笔记
java·笔记·spring·微服务
SimonKing4 小时前
惊!未实现Serializable竟让第三方接口回调全军覆没
前端·程序员·架构