核心内容摘要
智能跨平台下载革命:Ghost Downloader 3突破传统下载体验
视频看了几百小时还迷糊关注我几分钟让你秒懂你是否好奇微信、淘宝、GitHub 的“扫码登录”是怎么实现的为什么手机确认后网页就自动登录了二维码里到底存了什么后端如何知道用户“已扫码”今天我们就用Spring Boot Vue从零实现一个高仿微信扫码登录系统包含✅ 生成带唯一 ID 的二维码✅ 手机端模拟扫码确认✅ 网页端轮询/长轮询检测状态✅ 登录成功跳转小白也能跟着做
扫码登录的核心原理扫码登录本质是“设备间通信”流程如下关键点二维码内容 一个临时唯一 IDscene_id手机扫码 向服务器报告 “这个 ID 被谁确认了”网页轮询 不断问 “这个 ID 有结果了吗”
技术选型模块技术后端Spring Boot
x Redis前端网页Vue 3 Axios qrcode.vue手机端模拟Postman / 另一个 API 调用二维码生成com.google.zxing会话存储Redis存 scene_id → user 关系
后端实现Spring Boot
添加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.google.zxing/groupId artifactIdjavase/artifactId version
3.
1/version /dependency
配置 Redisapplication.ymlspring: redis: host: localhost port:
核心接口定义RestController RequestMapping(/scan) public class ScanLoginController { Autowired private RedisTemplateString, String redisTemplate; private static final String SCENE_PREFIX scan_login:; private static final long EXPIRE_SECONDS 300; // 5分钟过期 /** *
网页端获取登录二维码 */ GetMapping(/qrcode) public ResponseEntitybyte[] getQrCode() throws Exception { // 生成唯一 scene_id String sceneId UUID.randomUUID().toString().replace(-, ); String key SCENE_PREFIX sceneId; // 存入 Redis初始状态为 created redisTemplate.opsForValue().set(key, created, EXPIRE_SECONDS, TimeUnit.SECONDS); // 二维码内容指向扫码确认接口的 URL含 scene_id String content http://localhost:8080/scan/confirm?sceneId sceneId; // 生成二维码图片PNG 字节数组 byte[] qrCodeImage QrCodeUtil.createQrCode(content, 300,
; return ResponseEntity.ok() .header(sceneId, sceneId) // 返回 sceneId 给前端 .contentType(MediaType.IMAGE_PNG) .body(qrCodeImage); } /** *
手机端扫码后确认登录模拟 */ PostMapping(/confirm) public String confirmLogin(RequestParam String sceneId, RequestParam String userId) { String key SCENE_PREFIX sceneId; Boolean exists redisTemplate.hasKey(key); if (Boolean.TRUE.equals(exists)) { // 将状态更新为用户ID表示已确认 redisTemplate.opsForValue().set(key, userId, EXPIRE_SECONDS, TimeUnit.SECONDS); return 确认成功; } return sceneId 无效或已过期; } /** *
网页端轮询查询登录状态 */ GetMapping(/status) public ResponseEntityMapString, Object checkStatus(RequestParam String sceneId) { String key SCENE_PREFIX sceneId; String value redisTemplate.opsForValue().get(key); MapString, Object result new HashMap(); if (value null) { result.put(status, expired); // 已过期 } else if (created.equals(value)) { result.put(status, waiting); // 等待扫码 } else { result.put(status, success); result.put(userId, value); // 登录用户ID // 可在此生成 JWT 或 Session } return ResponseEntity.ok(result); } }
二维码工具类public class QrCodeUtil { public static byte[] createQrCode(String content, int width, int height) throws Exception { BitMatrix bitMatrix new MultiFormatWriter() .encode(content, BarcodeFormat.QR_CODE, width, height); ByteArrayOutputStream pngOutputStream new ByteArrayOutputStream(); MatrixToImageWriter.writeToStream(bitMatrix, PNG, pngOutputStream); return pngOutputStream.toByteArray(); } }
前端实现Vue
安装依赖npm install axios qrcode.vue
扫码登录页面ScanLogin.vuetemplate div v-if!isLoggedIn h2扫码登录/h2 div v-ifqrCodeUrl !-- 显示二维码 -- img :srcqrCodeUrl alt扫码登录 / p/p /div div v-else加载中.../div /div div v-else h2登录成功欢迎 /h2 button clicklogout退出/button /div /template script setup import { ref, onMounted } from vue import axios from axios const qrCodeUrl ref() const sceneId ref() const statusText ref(请使用手机扫码) const isLoggedIn ref(false) const userId ref() // 获取二维码 const fetchQrCode async () { const res await axios.get(/scan/qrcode, { responseType: blob }) sceneId.value res.headers[sceneid] // 从响应头获取 sceneId qrCodeUrl.value URL.createObjectURL(res.data) pollStatus() // 开始轮询 } // 轮询检查状态 const pollStatus () { const timer setInterval(async () { try { const res await axios.get(/scan/status?sceneId${sceneId.value}) const { status, userId: uid } res.data if (status success) { clearInterval(timer) userId.value uid isLoggedIn.value true statusText.value 登录成功 } else if (status expired) { clearInterval(timer) statusText.value 二维码已过期请刷新重试 } else { statusText.value 等待扫码... } } catch (e) { console.error(e) } },
// 每2秒查一次 } const logout () { isLoggedIn.value false qrCodeUrl.value fetchQrCode() } onMounted(() { fetchQrCode() }) /script
模拟手机扫码测试用打开网页看到二维码用 Postman 或 curl 模拟手机扫码确认curl -X POST http://localhost:8080/scan/confirm?sceneId你的sceneIduserId10001网页端2 秒内自动跳转到“登录成功”页面⚠️
反例
注意事项❌ 反例1用数据库代替 Redis数据库写入慢高并发下性能差必须用 Redis 这类内存数据库支持高频率读写。
❌ 反例2scene_id 不设过期用户关闭页面后scene_id 永久占用内存务必设置 TTL如 5 分钟。
❌ 反例3二维码内容直接放用户信息// 错误 String content userId10001;后果任何人扫了都能登录✅ 正确做法二维码只放临时 scene_id身份由手机端安全上报。
✅ 安全加固建议scene_id 用 UUID不可预测确认接口加鉴权如手机端需先登录同一 scene_id 只能使用一次确认后立即删除或标记生产环境用 HTTPS防止中间人攻击。
进阶优化方向优化点方案减少轮询压力改用 WebSocket 或 SSE服务端推送支持多端登录scene_id 绑定设备类型防刷机制限制 IP 每分钟生成二维码次数日志追踪记录 scene_id 生命周期
总结扫码登录 临时 ID 状态同步二维码内容 ≠ 用户信息而是回调地址 scene_id核心数据结构Redis 中scene_id → user_id网页通过轮询感知状态变化安全关键scene_id 随机、短期有效、确认接口受保护。
掌握这套逻辑你不仅能实现扫码登录还能扩展到扫码支付、扫码授权等场景视频看了几百小时还迷糊关注我几分钟让你秒懂