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的映射实现精准推送。🚀
