视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
你是否好奇:微信、淘宝、GitHub 的“扫码登录”是怎么实现的?
- 为什么手机确认后,网页就自动登录了?
- 二维码里到底存了什么?
- 后端如何知道用户“已扫码”?
今天我们就用Spring Boot + Vue,从零实现一个高仿微信扫码登录系统,包含:
✅ 生成带唯一 ID 的二维码
✅ 手机端模拟扫码确认
✅ 网页端轮询/长轮询检测状态
✅ 登录成功跳转
小白也能跟着做!
🧩 一、扫码登录的核心原理
扫码登录本质是“设备间通信”,流程如下:
🔑关键点:
- 二维码内容 = 一个临时唯一 ID(scene_id)
- 手机扫码 = 向服务器报告 “这个 ID 被谁确认了”
- 网页轮询 = 不断问 “这个 ID 有结果了吗?”
🔧 二、技术选型
| 模块 | 技术 |
|---|---|
| 后端 | Spring Boot 3.x + Redis |
| 前端(网页) | Vue 3 + Axios + qrcode.vue |
| 手机端(模拟) | Postman / 另一个 API 调用 |
| 二维码生成 | com.google.zxing |
| 会话存储 | Redis(存 scene_id → user 关系) |
💻 三、后端实现(Spring Boot)
1. 添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.5.1</version> </dependency>2. 配置 Redis(application.yml)
spring: redis: host: localhost port: 63793. 核心接口定义
@RestController @RequestMapping("/scan") public class ScanLoginController { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String SCENE_PREFIX = "scan_login:"; private static final long EXPIRE_SECONDS = 300; // 5分钟过期 /** * 1. 网页端:获取登录二维码 */ @GetMapping("/qrcode") public ResponseEntity<byte[]> 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, 300); return ResponseEntity.ok() .header("sceneId", sceneId) // 返回 sceneId 给前端 .contentType(MediaType.IMAGE_PNG) .body(qrCodeImage); } /** * 2. 手机端:扫码后确认登录(模拟) */ @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 无效或已过期"; } /** * 3. 网页端:轮询查询登录状态 */ @GetMapping("/status") public ResponseEntity<Map<String, Object>> checkStatus(@RequestParam String sceneId) { String key = SCENE_PREFIX + sceneId; String value = redisTemplate.opsForValue().get(key); Map<String, 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); } }4. 二维码工具类
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 3)
1. 安装依赖
npm install axios qrcode.vue2. 扫码登录页面(ScanLogin.vue)
<template> <div v-if="!isLoggedIn"> <h2>扫码登录</h2> <div v-if="qrCodeUrl"> <!-- 显示二维码 --> <img :src="qrCodeUrl" alt="扫码登录" /> <p>{{ statusText }}</p> </div> <div v-else>加载中...</div> </div> <div v-else> <h2>登录成功!欢迎 {{ userId }}</h2> <button @click="logout">退出</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) } }, 2000) // 每2秒查一次 } const logout = () => { isLoggedIn.value = false qrCodeUrl.value = '' fetchQrCode() } onMounted(() => { fetchQrCode() }) </script>📱 五、模拟手机扫码(测试用)
- 打开网页,看到二维码;
- 用 Postman 或 curl 模拟手机扫码确认:
curl -X POST "http://localhost:8080/scan/confirm?sceneId=你的sceneId&userId=10001"- 网页端2 秒内自动跳转到“登录成功”页面!
⚠️ 六、反例 & 注意事项
❌ 反例1:用数据库代替 Redis
- 数据库写入慢,高并发下性能差;
- 必须用 Redis 这类内存数据库,支持高频率读写。
❌ 反例2:scene_id 不设过期
- 用户关闭页面后,scene_id 永久占用内存;
- 务必设置 TTL(如 5 分钟)。
❌ 反例3:二维码内容直接放用户信息
// 错误! String content = "userId=10001";后果:任何人扫了都能登录!
✅ 正确做法:二维码只放临时 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 随机、短期有效、确认接口受保护。
掌握这套逻辑,你不仅能实现扫码登录,还能扩展到扫码支付、扫码授权等场景!
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!