扫码登录 WebSocket Session 流程和知识点。 完整流程梳理1. 前端建立 WebSocket 连接// 前端生成唯一 scanCodeconstscanCodegenerateUUID();// 例如: abc-123-xyz// 建立 WebSocket 连接无 TokenconstwsnewWebSocket(ws://localhost:48080/system/ws?scanCodeabc-123-xyz);ws.onopen(){// 发送注册消息ws.send(JSON.stringify({type:scan-login-register,content:JSON.stringify({scanCode:abc-123-xyz})}));};2. 后端处理连接建立浏览器 → Gateway (48080) → system-server (48081)步骤 2.1握手拦截器提取 scanCode[LoginUserHandshakeInterceptor.java](file:///F:/JavaProgram/cloud/jlk-framework/jlk-spring-boot-starter-websocket/src/main/java/cn/teaching/jlk/framework/websocket/core/security/LoginUserHandshakeInterceptor.java#L26-L40)OverridepublicbooleanbeforeHandshake(ServerHttpRequestrequest,...){// 提取 scanCode 参数StringscanCodeextractScanCode(request);if(scanCode!null){attributes.put(SCAN_CODE,scanCode);}returntrue;}步骤 2.2自动注册 Session[WebSocketSessionHandlerDecorator.java](file:///F:/JavaProgram/cloud/jlk-framework/jlk-spring-boot-starter-websocket/src/main/java/cn/teaching/jlk/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java#L37-L42)OverridepublicvoidafterConnectionEstablished(WebSocketSessionsession){// 包装为支持并发的 SessionsessionnewConcurrentWebSocketSessionDecorator(session,...);// ⭐ 自动添加到 WebSocketSessionManagersessionManager.addSession(session);}此时WebSocketSessionManager中存储idSessions: { bc89a7af-430a-fcbb-065a-a4f7bdf110a4 → WebSocketSession对象 }3. 前端发送注册消息ws.send(JSON.stringify({type:scan-login-register,content:JSON.stringify({scanCode:abc-123-xyz})}));4. 后端处理注册消息[ScanLoginWebSocketMessageListener.java](file:///F:/JavaProgram/cloud/jlk-module-system/jlk-module-system-server/src/main/java/cn/teaching/jlk/module/system/websocket/ScanLoginWebSocketMessageListener.java#L30-L45)OverridepublicvoidonMessage(WebSocketSessionsession,ScanLoginRegisterMessagemessage){StringscanCodemessage.getScanCode();StringsessionIdsession.getId();// bc89a7af-...// ⭐ 关键将 scanCode 与 sessionId 映射存储到 RedisscanLoginService.registerScanCode(scanCode,sessionId);// 返回确认消息webSocketMessageSender.sendObject(sessionId,scan-login-register-response,response);}此时 Redis 中存储Key: scan_login:abc-123-xyz Value: bc89a7af-430a-fcbb-065a-a4f7bdf110a4 TTL: 300秒5分钟5. APP 扫码确认POST /system/auth/scan-login/confirm { scanCode: abc-123-xyz, userId: 123 }[ScanLoginServiceImpl.java](file:///F:/JavaProgram/cloud/jlk-module-system/jlk-module-system-server/src/main/java/cn/teaching/jlk/module/system/service/auth/ScanLoginServiceImpl.java#L58-L88)publicCommonResultBooleanconfirmScanLogin(StringscanCode,LonguserId){// 1. 从 Redis 获取 sessionIdStringsessionIdredisTemplate.opsForValue().get(scan_login:abc-123-xyz);// sessionId bc89a7af-430a-fcbb-065a-a4f7bdf110a4// 2. 生成 TokenOAuth2AccessTokenDOtokenoauth2TokenService.createAccessToken(...);// 3. ⭐ 通过 sessionId 查找 WebSocket Session 并推送webSocketMessageSender.sendObject(sessionId,scan-login-success,response);// 4. 删除 Redis 数据防止重复使用redisTemplate.delete(scan_login:abc-123-xyz);}6. 前端收到登录信息ws.onmessage(event){constmsgJSON.parse(event.data);if(msg.typescan-login-success){constloginDataJSON.parse(msg.content);// 保存 TokenlocalStorage.setItem(token,loginData.accessToken);// 跳转到首页router.push(/home);}}; WebSocket Session 核心知识点1. Session 的生命周期创建 → 注册 → 使用 → 销毁阶段触发时机操作创建前端new WebSocket()浏览器发起 TCP HTTP Upgrade注册afterConnectionEstablished自动添加到WebSocketSessionManager使用接收/发送消息通过sessionId查找 Session销毁连接关闭/超时自动从WebSocketSessionManager移除2. Session 存储结构[WebSocketSessionManagerImpl.java](file:///F:/JavaProgram/cloud/jlk-framework/jlk-spring-boot-starter-websocket/src/main/java/cn/teaching/jlk/framework/websocket/core/session/WebSocketSessionManagerImpl.java#L31-L40)// 1. 按 sessionId 存储ConcurrentMapString,WebSocketSessionidSessionsnewConcurrentHashMap();// 2. 按用户类型 用户ID 存储登录后才有ConcurrentMapInteger,ConcurrentMapLong,CopyOnWriteArrayListWebSocketSessionuserSessions;扫码登录时的特殊情况// 未登录时user null只存储在 idSessions 中[addSession][sessionIdxxx,userIdnull,userTypenull,tenantIdnull]3. Session ID 的生成sessionId UUID 格式 例如: bc89a7af-430a-fcbb-065a-a4f7bdf110a4由 Spring WebSocket 框架自动生成每个连接都有唯一的 sessionId用于在WebSocketSessionManager中查找 Session4. Session 的作用域⚠️重要Session 是服务实例级别的system-server 实例1 (48081) └─ WebSocketSessionManager └─ idSessions: { session-1 → ..., session-2 → ... } infra-server 实例1 (48082) └─ WebSocketSessionManager └─ idSessions: { session-3 → ..., session-4 → ... }这就是为什么之前跨服务调用会失败的原因 回答你的问题Q1: 这是单通道吗答不是单通道是多通道用户A 浏览器 → ws://.../system/ws?scanCodeA-xxx → Session-A (sessionId: abc-123) 用户B 浏览器 → ws://.../system/ws?scanCodeB-yyy → Session-B (sessionId: def-456) 用户C 浏览器 → ws://.../system/ws?scanCodeC-zzz → Session-C (sessionId: ghi-789)每个用户都会建立一个独立的 WebSocket 连接通道Q2: 每个用户进来扫码都会建立一个全新的通道吗答是的完全正确时间线 T1: 用户A 打开网页 → 建立 Session-A → 显示二维码 T2: 用户B 打开网页 → 建立 Session-B → 显示二维码 T3: 用户C 打开网页 → 建立 Session-C → 显示二维码 T4: 用户A 扫码 → 通过 Session-A 推送登录信息 ✅ T5: 用户B 扫码 → 通过 Session-B 推送登录信息 ✅ T6: 用户C 扫码 → 通过 Session-C 推送登录信息 ✅关键点每个浏览器标签页 一个独立的 WebSocket 连接每个连接有唯一的sessionId通过scanCode → sessionId的映射确保消息推送到正确的用户Q3: Session 什么时候销毁// 情况1用户主动关闭页面window.onbeforeunload(){ws.close();// Session 被移除};// 情况2网络断开ws.onclose(event){console.log(连接关闭:,event.code);// Session 被移除};// 情况3长时间无心跳框架默认配置// 通常 60-300 秒后自动断开后端自动清理OverridepublicvoidafterConnectionClosed(WebSocketSessionsession,CloseStatuscloseStatus){sessionManager.removeSession(session);// ⭐ 自动移除} 并发场景分析场景1同一用户多个标签页用户A - 标签页1 → Session-A1 (scanCode: A-111) 用户A - 标签页2 → Session-A2 (scanCode: A-222) APP 扫描 A-111 → 只有标签页1 收到登录信息 ✅ APP 扫描 A-222 → 只有标签页2 收到登录信息 ✅场景2扫码过期T0: 前端建立连接 → Redis 存储 scanCode → TTL300秒 T300: 5分钟后未扫码 → Redis 自动删除 T301: APP 扫码 → Redis 查询返回 null → 提示二维码已过期场景3重复扫码T0: APP 第一次扫码 → 推送成功 → Redis 删除 scanCode T1: APP 第二次扫码 → Redis 查询返回 null → 拒绝重复登录 ✅ 安全性设计1. scanCode 一次性使用// 推送成功后立即删除stringRedisTemplate.delete(redisKey);2. 过期时间控制// 5分钟过期stringRedisTemplate.opsForValue().set(redisKey,sessionId,300,TimeUnit.SECONDS);3. 无 Token 连接的安全性风险未认证的 WebSocket 连接可能被滥用 缓解措施 ✅ scanCode 随机生成UUID难以猜测 ✅ 5分钟自动过期 ✅ 一次性使用用后即删 ✅ 连接建立后需发送注册消息才生效 总结特性说明通道数量多通道每个用户一个独立连接Session 存储服务本地内存ConcurrentHashMapSession 查找通过sessionId精确匹配跨服务调用❌ 不可行Session 不共享会话隔离✅ 完全隔离互不影响并发支持✅ 使用ConcurrentWebSocketSessionDecorator自动清理✅ 连接关闭时自动移除这就是完整的 WebSocket Session 机制每个用户都是独立的通道通过scanCode → sessionId的映射实现精准推送。