当前位置: 首页 > news >正文

spring websocket实现扫码登录

扫码登录 WebSocket Session 流程和知识点。

📊 完整流程梳理

1. 前端建立 WebSocket 连接

// 前端生成唯一 scanCodeconstscanCode=generateUUID();// 例如: "abc-123-xyz"// 建立 WebSocket 连接(无 Token)constws=newWebSocket('ws://localhost:48080/system/ws?scanCode=abc-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 参数StringscanCode=extractScanCode(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){// 包装为支持并发的 Sessionsession=newConcurrentWebSocketSessionDecorator(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){StringscanCode=message.getScanCode();StringsessionId=session.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)

publicCommonResult<Boolean>confirmScanLogin(StringscanCode,LonguserId){// 1. 从 Redis 获取 sessionIdStringsessionId=redisTemplate.opsForValue().get("scan_login:abc-123-xyz");// sessionId = "bc89a7af-430a-fcbb-065a-a4f7bdf110a4"// 2. 生成 TokenOAuth2AccessTokenDOtoken=oauth2TokenService.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)=>{constmsg=JSON.parse(event.data);if(msg.type==='scan-login-success'){constloginData=JSON.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 存储ConcurrentMap<String,WebSocketSession>idSessions=newConcurrentHashMap<>();// 2. 按用户类型 + 用户ID 存储(登录后才有)ConcurrentMap<Integer,ConcurrentMap<Long,CopyOnWriteArrayList<WebSocketSession>>>userSessions;

扫码登录时的特殊情况:

// 未登录时,user = null,只存储在 idSessions 中[addSession][sessionId=xxx,userId=null,userType=null,tenantId=null]

3. Session ID 的生成

sessionId = UUID 格式 例如: "bc89a7af-430a-fcbb-065a-a4f7bdf110a4"
  • 由 Spring WebSocket 框架自动生成
  • 每个连接都有唯一的 sessionId
  • 用于在WebSocketSessionManager中查找 Session

4. 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?scanCode=A-xxx → Session-A (sessionId: abc-123) 用户B 浏览器 → ws://.../system/ws?scanCode=B-yyy → Session-B (sessionId: def-456) 用户C 浏览器 → ws://.../system/ws?scanCode=C-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 → TTL=300秒 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 存储服务本地内存(ConcurrentHashMap)
Session 查找通过sessionId精确匹配
跨服务调用❌ 不可行,Session 不共享
会话隔离✅ 完全隔离,互不影响
并发支持✅ 使用ConcurrentWebSocketSessionDecorator
自动清理✅ 连接关闭时自动移除

这就是完整的 WebSocket Session 机制!每个用户都是独立的通道,通过scanCode → sessionId的映射实现精准推送。🚀

http://www.jsqmd.com/news/957542/

相关文章:

  • 2026年分体式超声波液位计十大品牌排行榜:国产技术突围与全场景选型实战指南 - 水质仪表品牌排行榜
  • 数据库语句 触发器 作业
  • 如何用PyVista实现专业级3D可视化:从数据到洞察的完整指南
  • 为什么我建议2026年程序员转行首先要选网络安全?
  • 遇上 Bug 别慌:用 GPT-5.5 + Claude 3.5 双重验证解决复杂代码报错
  • 微信与手机厂商合作推A2A助手,超级App与系统级AI助手争夺AI时代入口
  • 如何在Linux系统上安装Realtek 8852AE Wi-Fi 6驱动:完整指南
  • 如何利用Chinese-Medical-DIALOGUE-Data构建智能医疗对话系统:5大关键技术解析
  • 如何5分钟完成配置:3DS平台终极宝可梦存档管理器完整指南
  • 从百度程序员被抓事件,聊聊企业内部数据防篡改的3个技术方案(附脚本审计思路)
  • 基于小程序的酒店管理系统毕业设计
  • 3大3D渲染范式革新:F3D如何重塑跨平台可视化技术栈
  • 2026年 重庆水处理药剂厂家推荐榜单:聚合氯化铝/聚丙烯酰胺/次氯酸钠/硫酸亚铁/氯酸钠/漂白粉品牌精选与深度解析 - 品牌企业推荐师(官方)
  • 国际EMBA怎么选?5大主流国际EMBA项目全方位对比分析
  • 中医舌诊用YOLO11舌苔识别工具:含BiFPN+SDI增强模块、标注数据集与可视化界面
  • GHelper终极指南:10MB替代Armoury Crate的华硕笔记本控制神器
  • XHS-Downloader:小红书作品批量下载工具全攻略
  • 2026年青海西宁市TOP5折扣力度大的家电门店,你了解几家?
  • 动态规划-0-1背包问题
  • 微信好友检测秘籍:3分钟发现谁悄悄删了你,彻底清理无效社交
  • 基于小程序的青年公寓服务平台毕设
  • 战略管理国际EMBA怎么选?2026五大顶尖项目深度解析
  • 2026年祛痘精华液哪家好:权威TOP5专业深度测评 - 13425704091
  • 提升到底有多大?GPT-5.5 编程实测:从零构建 Web 应用的效率极限
  • 终极解决方案:CAD Sketcher 0.27.6安装失败问题深度剖析与修复指南
  • 跨境支付AML漏检率骤降81%的秘密(某国有大行Gemini私有化部署内部技术备忘录节选)
  • 2026年干皮适用的精华液哪家好:独家榜单官方深度测评 - 13425704091
  • 2026年青春期精华液哪家好:专业TOP5深度解析指南 - 17322238651
  • Matlab版钢筋腐蚀率智能预测工具:拖拽导入数据、调参训练、结果可视化一键完成
  • 搬了两次,才算真正搬完——一次装修过渡期搬迁的完整记录 - 知行集录