WebSocket握手认证与Netty实现详解 1. WebSocket 握手认证的核心逻辑WebSocket 握手认证的核心在于利用 HTTP 升级请求的时机进行身份验证。当客户端发起 WebSocket 连接时首先会发送一个 HTTP Upgrade 请求此时连接仍然是基于 HTTP 协议的。这个阶段我们可以像处理普通 HTTP 请求一样通过 URL 参数、Header 或 Cookie 等方式获取认证凭证。关键提示WebSocket 协议升级完成后就无法再使用 HTTP 的认证机制了所以必须在握手阶段完成所有认证工作。1.1 认证流程设计完整的认证流程包含以下几个关键步骤客户端发起 WebSocket 连接请求携带认证 Token服务端拦截 HTTP 握手请求从请求中提取 Token 并进行验证验证通过后绑定用户身份到连接通道清理 URI 中的认证参数完成协议升级建立 WebSocket 连接这种设计有以下几个优势认证逻辑与业务逻辑解耦一次认证全程有效支持多种 Token 携带方式失败时立即断开连接避免资源浪费2. Netty 实现细节解析2.1 核心处理器设计我们继承 ChannelInboundHandlerAdapter 实现自定义的握手认证处理器public class WsAuthHandshakeHandler extends ChannelInboundHandlerAdapter { private static final AttributeKeyString USER_ID_ATTR AttributeKey.valueOf(USER_ID); Override public void channelRead(ChannelHandlerContext ctx, Object object) { if (!(object instanceof FullHttpRequest req)) { ctx.fireChannelRead(object); return; } // 认证逻辑实现... } }这个处理器需要放置在 WebSocketServerProtocolHandler 之前确保在协议升级前完成认证。2.2 Token 提取与验证从请求中提取 Token 的几种常见方式URL 参数最常用String token UrlBuilder.ofHttp(req.uri()) .getQuery() .get(token);HTTP HeaderString token req.headers().get(X-Auth-Token);CookieString cookieHeader req.headers().get(HttpHeaderNames.COOKIE); // 解析 Cookie 获取 Token实际项目中建议采用多种方式组合提高兼容性。例如先检查 URL 参数没有则检查 Header最后检查 Cookie。2.3 用户身份绑定认证通过后将用户信息绑定到 ChannelString userId (String) StpUtil.getLoginIdByToken(token); ctx.channel().attr(USER_ID_ATTR).set(userId);后续处理器可以通过以下方式获取用户信息String userId ctx.channel().attr(USER_ID_ATTR).get();3. Sa-Token 集成实践3.1 Sa-Token 配置要点在 Spring Boot 项目中集成 Sa-Token 需要以下配置添加依赖dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version最新版本/version /dependency基础配置application.ymlsa-token: token-name: auth-token # Token名称 timeout: 86400 # Token有效期(秒) is-share: true # 是否共享Cookie is-read-body: false # 是否从请求体读取Token3.2 Token 验证逻辑Sa-Token 提供了简洁的 API 进行 Token 验证try { String userId (String) StpUtil.getLoginIdByToken(token); // 验证成功处理... } catch (NotLoginException e) { // 验证失败处理... throw new RuntimeException(Token验证失败, e); }注意捕获 NotLoginException 异常它表示 Token 无效、过期或不存在。4. 完整实现与测试4.1 服务端完整配置Netty 服务端的关键配置ServerBootstrap bootstrap new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializerChannel() { Override protected void initChannel(Channel ch) { ChannelPipeline p ch.pipeline(); // HTTP 编解码器 p.addLast(new HttpServerCodec()); // 大数据流支持 p.addLast(new ChunkedWriteHandler()); // HTTP 消息聚合 p.addLast(new HttpObjectAggregator(65535)); // 自定义认证处理器 p.addLast(new WsAuthHandshakeHandler()); // WebSocket 协议处理器 p.addLast(new WebSocketServerProtocolHandler(/ws, null, true)); // 业务处理器 p.addLast(new WebSocketFrameHandler()); } });4.2 客户端连接测试使用 Apifox 测试 WebSocket 连接获取有效 Token通过登录接口建立 WebSocket 连接ws://localhost:8080/ws?token你的Token测试不带 Token 的连接应被拒绝测试带无效 Token 的连接应被拒绝4.3 性能优化建议Token 缓存将验证过的 Token 缓存起来避免重复查询数据库连接池为 Sa-Token 的存储层配置连接池异步验证对于高并发场景考虑使用异步方式验证 Token批量验证支持批量验证多个 Token减少网络开销5. 常见问题与解决方案5.1 认证失败场景处理问题现象可能原因解决方案401 Unauthorized未携带 Token检查客户端是否正确发送 Token401 UnauthorizedToken 过期刷新 Token 或重新登录401 UnauthorizedToken 格式错误检查 Token 生成和解析逻辑404 Not Found路径不匹配检查服务端和客户端的路径配置5.2 连接稳定性问题心跳机制// 添加心跳处理器 pipeline.addLast(new IdleStateHandler(60, 0, 0)); pipeline.addLast(new HeartbeatHandler());断线重连客户端应实现自动重连逻辑服务端可记录连接状态支持会话恢复资源泄漏确保正确关闭连接使用 Netty 的泄漏检测工具5.3 安全性增强Token 加密对传输的 Token 进行加密IP 绑定将 Token 与客户端 IP 绑定频率限制防止暴力破解HTTPS生产环境必须使用 WSS6. 高级应用场景6.1 多端登录管理利用 Sa-Token 的会话管理功能// 同账号互斥登录 StpUtil.login(userId, PC); // 查询所有登录设备 ListSaSession sessions StpUtil.searchSession(login-device:PC, 0, 10);6.2 消息广播认证后可以实现定向消息推送// 获取所有已认证的连接 SetChannel channels ChannelManager.getAuthenticatedChannels(); // 向特定用户发送消息 channels.stream() .filter(ch - userId.equals(ch.attr(USER_ID_ATTR).get())) .forEach(ch - ch.writeAndFlush(new TextWebSocketFrame(message)));6.3 负载均衡方案Token 共享所有节点共享同一个 Token 存储会话同步通过 Redis 等中间件同步会话信息粘性会话使用 Nginx 的 ip_hash 策略在实际项目中我曾遇到过 Token 验证性能瓶颈的问题。通过引入本地缓存将验证过的 Token 缓存 5 分钟使系统吞吐量提升了 3 倍。关键实现如下// 使用 Caffeine 缓存 CacheString, String tokenCache Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .maximumSize(10000) .build(); String userId tokenCache.get(token, t - { try { return (String) StpUtil.getLoginIdByToken(t); } catch (NotLoginException e) { return null; } });这种优化特别适合高并发场景但需要注意缓存一致性问题当用户主动注销时需要及时清除缓存。