一场关于二维码的"扫盲"课,看完秒变登录专家
"小陈啊,过来一下!"老王端着枸杞保温杯,神秘兮兮地招手。
小陈从代码堆里抬起头:"领导,是不是又要改需求?"
老王嘿嘿一笑:"今天给你讲讲扫码登录的原理,这可是现代开发的必备技能!"
小陈眼睛一亮:"这个我感兴趣!"
第一章:二维码的诞生——从前端的第一次请求开始
老王清清嗓子:"首先,咱们的前端需要向后台请求生成一个二维码..."
// 前端:请求生成二维码(与 demo 对应)
asyncfunction requestQRCode() {
try {
const response = await fetch('/api/session', { method: 'POST' });
if (!response.ok) thrownewError('网络请求失败');
const data = await response.json();
// demo 返回的数据结构示例:
// {
// id: '123e4567-e89b-12d3-a456-426614174000',
// qrDataUrl: 'data:image/png;base64,...' // 二维码的 data URL
// }
// 显示二维码(这里使用 data URL)
displayQRCode(data.qrDataUrl);
// 用返回的 id 开始轮询(demo 使用 id 字段)
startPolling(data.id);
return data;
} catch (error) {
console.error('生成二维码失败:', error);
showError('生成二维码失败,请刷新重试');
}
}
小陈点点头:"所以前端先找后台要个二维码,后台生成后返回图片地址和唯一ID。"
第二章:二维码的秘密——里面到底藏了什么?
老王神秘地说:"这个二维码里其实编码了一个特殊URL,比如:https://example.com/login?token=QR_CODE_ID&type=login"
"手机APP扫描后,"老王继续道,"会解析出里面的token,然后告诉服务器:'嗨,我扫了这个码!'"
// 手机APP端(简化示例,与 demo 对应)
function onQRCodeScanned(qrCodeContent) {
try {
const url = new URL(qrCodeContent);
// demo 在二维码中使用 query 参数 session=<id>
const sessionId = url.searchParams.get('session');
const type = url.searchParams.get('type');
if (type === 'login' || sessionId) {
// 告诉服务器:这个 session 被扫描了(POST /api/scan/:id)
fetch(`/api/scan/${sessionId}`, { method: 'POST' }).then(() => {
// 显示确认界面
showConfirmationDialog('是否确认登录网页端?', () => {
// 用户点击确认后
confirmLogin(sessionId);
});
});
}
} catch (error) {
showToast('无效的二维码');
}
}
// 用户确认登录(与 demo 对应:POST /api/confirm/:id)
asyncfunction confirmLogin(sessionId) {
try {
const res = await fetch(`/api/confirm/${sessionId}`, { method: 'POST' });
const result = await res.json();
if (res.ok) {
showToast('登录成功!');
} else {
showToast('登录失败:' + (result.error || '未知错误'));
}
} catch (error) {
showToast('登录失败,请重试');
}
}
第三章:轮询的艺术——前端如何知道被扫了?
老王眨眨眼:"最精彩的部分来了!网页怎么知道用户已经扫描了呢?"
"不断问!"小陈抢答。
"没错!"老王拍桌,"前端会定时询问后台:'有人扫了我吗?有人爱我吗?'"
// 前端轮询检查状态(与 demo 对应)
class QRLoginPoller {
constructor(qrCodeId) {
this.qrCodeId = qrCodeId;
this.intervalId = null;
this.pollCount = 0;
this.maxPollCount = 150; // 5分钟(2秒一次)
}
// 开始轮询
start() {
this.intervalId = setInterval(() =>this.checkStatus(), 2000);
}
// 检查状态(demo 的接口为 GET /api/status/:id,返回 { id, status })
async checkStatus() {
if (this.pollCount++ >= this.maxPollCount) {
this.stop();
this.onExpired();
return;
}
try {
const response = await fetch(`/api/status/${this.qrCodeId}`);
if (!response.ok) thrownewError('状态获取失败');
const j = await response.json();
const state = j.status; // 'pending' | 'scanned' | 'confirmed' | 'expired'
this.handleState(state, j);
} catch (error) {
console.error('轮询请求失败:', error);
}
}
handleState(state, payload) {
switch (state) {
case'scanned':
this.onScanned();
break;
case'confirmed':
// demo 不返回 token;如果返回 token,可从 payload.token 读取
this.onConfirmed(payload.token);
break;
case'expired':
this.onExpired();
break;
default:
// waiting / pending: 继续轮询
break;
}
}
onScanned() {
updateUI('已扫描,请确认登录');
}
onConfirmed(token) {
this.stop();
if (token) saveAuthToken(token);
updateUI('登录成功!');
redirectToHomePage();
}
onExpired() {
this.stop();
updateUI('二维码已过期');
showRefreshButton();
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
第四章:安全大战——如何防止被"劫持"?
老王严肃起来:"安全问题很重要!我们来聊聊防护措施..."
小陈接话:"1. 超时机制:5分钟过期
2. 一次性使用:每个码只能用一次
- 需要用户确认:防止意外扫描
- 设备验证:防止跨设备攻击"
// 服务端安全验证
class QRCodeSecurity {
constructor() {
this.qrCodeStore = newMap();
}
// 生成二维码
generateQRCode() {
const qrCodeId = this.generateUniqueId();
const expiresAt = Date.now() + 5 * 60 * 1000;
const qrCodeInfo = {
id: qrCodeId,
status: 'waiting', // waiting, scanned, confirmed, expired
expiresAt: expiresAt,
scannedAt: null,
confirmedAt: null,
userId: null,
deviceInfo: null,
token: null
};
this.qrCodeStore.set(qrCodeId, qrCodeInfo);
return {
qrCodeId,
expiresIn: 300
};
}
// 验证扫描请求
validateScan(qrCodeId, deviceInfo) {
const info = this.qrCodeStore.get(qrCodeId);
if (!info) {
thrownewError('二维码不存在');
}
if (info.status !== 'waiting') {
thrownewError('二维码状态无效');
}
if (Date.now() > info.expiresAt) {
info.status = 'expired';
thrownewError('二维码已过期');
}
// 记录设备信息
info.deviceInfo = deviceInfo;
info.status = 'scanned';
info.scannedAt = Date.now();
returntrue;
}
// 验证确认请求
validateConfirm(qrCodeId, userId) {
const info = this.qrCodeStore.get(qrCodeId);
// ...各种验证
info.status = 'confirmed';
info.userId = userId;
info.confirmedAt = Date.now();
info.token = this.generateAuthToken(userId);
return info.token;
}
}
第五章:大功告成——登录成功的狂欢!
老王总结道:"当所有验证通过后,前端拿到token,就可以完成登录了!"
// 登录成功处理
function handleLoginSuccess(token) {
// 1. 存储token
localStorage.setItem('auth_token', token);
// 2. 设置axios拦截器
axios.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});
// 3. 更新UI状态
updateUserInterface(true);
// 4. 跳转页面
navigateTo('/dashboard');
// 5. 显示欢迎消息
showNotification('登录成功!欢迎回来');
}
// 完整的登录流程
asyncfunction completeQRLogin() {
try {
// 1. 生成二维码
const qrData = await generateQRCode();
// 2. 显示二维码
displayQRCode(qrData.qrCodeUrl);
// 3. 开始轮询
const poller = new QRLoginPoller(qrData.qrCodeId);
poller.on('confirmed', (token) => {
handleLoginSuccess(token);
});
poller.start();
} catch (error) {
showError(`登录失败: ${error.message}`);
}
}
结局:意想不到的反转
老王得意地问:"怎么样?听懂了吗?"
小陈微微一笑:"领导,您讲得真好!不过..."
"不过什么?"
"我上周刚好看了微信扫码登录的源码分析,您要不再给我讲讲WebSocket优化轮询的方案?"
老王:"......今天的会就开到这吧!"
完整代码见:
总结一下扫码登录的步骤:
- 前端请求 → 后台生成二维码和唯一ID
- 显示二维码 → 用户打开手机APP扫描
- APP解析 → 向服务器报告扫描事件
- 用户确认 → APP发送确认登录请求
- 前端轮询 → 不断检查登录状态
- 获取token → 登录成功,维持会话
下次扫码时,想想背后这场精密的"对话芭蕾",是不是觉得技术真的很神奇呢?
记得点赞关注,下期带你用WebSocket优化这个流程!
我是用故事讲技术的【小枫学幽默】确定不关注一下?