"不就是生成二维码让用户扫吗?" ------这是我接到需求时的天真想法
"这破功能居然要考虑这么多细节?"------这是我开发三天后的真实状态
一、扫码登录的本质认知(血泪教训)
1. 表面流程
graph LR
A[网页生成二维码] --> B[手机扫码]
B --> C[手机确认登录]
C --> D[网页自动跳转]
2. 实际隐藏的复杂流程
graph TD
A[生成唯一UUID] --> B[绑定二维码状态]
B --> C[WebSocket长连接]
C --> D[轮询检查状态]
D --> E[Token安全校验]
E --> F[跨设备信息同步]
F --> G[登录态维持]
二、我的实现方案(不断推翻重来版)
1. 第一版:简单轮询(新手村版本)
javascript
// 前端代码(灾难现场)
let timer = setInterval(() => {
fetch('/check-login?qrid=123').then(res => {
if(res.status === 'SCANNED') {
alert('请点击确认!')
} else if(res.status === 'CONFIRMED') {
clearInterval(timer)
}
})
}, 3000) // 无脑3秒请求一次
踩坑记录:
- 手机端确认后,PC端最长需要等待3秒才跳转(用户以为卡死了)
- 同时打开多个页面会导致轮询混乱
- 没有处理断网重连机制
2. 第二版:WebSocket升级(稍有进步)
javascript
const ws = new WebSocket('wss://api.example.com/qrcode/123')
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
switch(data.status) {
case 'SCANNED':
showUserAvatar(data.userInfo) // 显示用户头像
break
case 'CONFIRMED':
localStorage.setItem('token', data.token)
break
}
}
踩坑记录:
- 未处理WebSocket自动重连(用户网络抖动直接掉线)
- 没有心跳检测机制(Nginx 60秒自动断开)
- 二维码刷新后旧的WebSocket仍在运行(产生幽灵连接)
三、最终稳定版架构(含泪总结)
1. 完整流程图

2. 核心代码片段
javascript
// 前端二维码生成
async function generateQRCode() {
const { qrid, expires } = await api.getQRCode()
const ws = initWebSocket(qrid) // 创建专属WebSocket
// 二维码过期处理
const expireTimer = setTimeout(() => {
ws.close()
showExpireAlert()
}, expires * 1000)
// 断网自动重连
ws.onclose = () => {
if (!isConfirmed) {
reconnectWebSocket(qrid)
}
}
}
// WebSocket管理
function initWebSocket(qrid) {
const ws = new WebSocket(`wss://api.com/qrcode/${qrid}`)
ws.onmessage = (event) => {
const { status, token, user } = JSON.parse(event.data)
switch(status) {
case 'SCANNED':
showScanSuccess(user.avatar)
break
case 'CONFIRMED':
clearTimeout(expireTimer)
handleLoginSuccess(token)
ws.close()
break
}
}
return ws
}
四、那些让我头秃的坑
1. 二维码刷新地狱
- 现象:用户刷新页面后新旧二维码同时生效
- 解决:在后端维护二维码状态机(pending/scanned/expired)
2. 跨域CORS问题
-
现象:WebSocket连接成功但收不到消息
-
根本原因 :未正确处理
Origin头校验 -
解决方案 :
nginx# Nginx配置 location /qrcode { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_pass http://backend; }
3. 安全漏洞(吓出冷汗)
- 风险点:中间人可能截获二维码ID伪造登录
- 防护措施 :
- 使用一次性UUID + IP绑定
- 扫码后需要二次确认(防止误扫)
- Token加入时间戳防重放攻击
五、性能优化小技巧
1. 二维码压缩方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 直接返回URL | 简单粗暴 | 可能暴露内部API |
| 后端生成Base64 | 安全可控 | 增加服务器压力 |
| 前端生成二维码 | 减轻后端负担 | 依赖qrcode.js等库 |
2. WebSocket心跳检测
javascript
// 每30秒发送心跳包
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'heartbeat' }))
}
}, 30000)
3. 优雅降级方案
javascript
// 检测WebSocket支持情况
if (!('WebSocket' in window)) {
fallbackToPolling() // 自动降级到长轮询
showBrowserUpgradeAlert()
}
六、写给后来者的建议
-
不要相信"已扫码"状态
用户可能扫码后取消确认,一定要有超时重置机制
-
手机端防抖设计
快速点击确认按钮可能导致重复提交
-
埋点监控必不可少
记录以下关键指标:
- 二维码生成量
- 扫码成功率
- 平均确认时间
- 失败原因统计
-
多设备测试
尤其注意:
- PC微信扫码跳转到手机微信的特殊处理
- 安卓/iOS的扫码速度差异
- 不同浏览器对WebSocket的支持
七、我的学习资料清单
- RFC6455 WebSocket协议规范(硬核但必要)
- 二维码生成原理科普
- OAuth 2.0 Device Flow(设计参考)
- 同事的咖啡(解决问题的最佳催化剂)
现在如果有人问我:"实现扫码登录要多久?"
我会回答:"给我两周,其中一周用来处理各种边界情况"
最后的忠告:当你觉得"应该没问题了"的时候,记得还有以下场景没测试:
- 扫码后断网
- 同时扫描多个二维码
- 手机确认后PC端切换标签页
- 用户在最后0.1秒点击取消
(别问我为什么知道这些...都是眼泪)