喂~妖妖零吗?这里有人把扫码登录原理讲得太明白了!

70 阅读3分钟

一场关于二维码的"扫盲"课,看完秒变登录专家

"小陈啊,过来一下!"老王端着枸杞保温杯,神秘兮兮地招手。

小陈从代码堆里抬起头:"领导,是不是又要改需求?"

老王嘿嘿一笑:"今天给你讲讲扫码登录的原理,这可是现代开发的必备技能!"

小陈眼睛一亮:"这个我感兴趣!"

第一章:二维码的诞生——从前端的第一次请求开始

老王清清嗓子:"首先,咱们的前端需要向后台请求生成一个二维码..."

// 前端:请求生成二维码(与 demo 对应)
asyncfunction requestQRCode() {
try {
    const response = await fetch('/api/session', { method'POST' });
    if (!response.okthrownewError('网络请求失败');
    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.intervalIdnull;
    this.pollCount0;
    this.maxPollCount150// 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.okthrownewError('状态获取失败');
      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.intervalIdnull;
    }
  }
}

图片图片

第四章:安全大战——如何防止被"劫持"?

老王严肃起来:"安全问题很重要!我们来聊聊防护措施..."

小陈接话:"1. 超时机制:5分钟过期
2. 一次性使用:每个码只能用一次

  1. 需要用户确认:防止意外扫描
  2. 设备验证:防止跨设备攻击"
// 服务端安全验证
class QRCodeSecurity {
constructor() {
    this.qrCodeStorenewMap();
  }

// 生成二维码
  generateQRCode() {
    const qrCodeId = this.generateUniqueId();
    const expiresAt = Date.now() + 5 * 60 * 1000;
    
    const qrCodeInfo = {
      id: qrCodeId,
      status'waiting'// waiting, scanned, confirmed, expired
      expiresAt: expiresAt,
      scannedAtnull,
      confirmedAtnull,
      userIdnull,
      deviceInfonull,
      tokennull
    };
    
    this.qrCodeStore.set(qrCodeId, qrCodeInfo);
    
    return {
      qrCodeId,
      expiresIn300
    };
  }

// 验证扫描请求
  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.scannedAtDate.now();
    
    returntrue;
  }

// 验证确认请求
  validateConfirm(qrCodeId, userId) {
    const info = this.qrCodeStore.get(qrCodeId);
    
    // ...各种验证
    
    info.status'confirmed';
    info.userId = userId;
    info.confirmedAtDate.now();
    info.tokenthis.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优化轮询的方案?"

老王:"......今天的会就开到这吧!"

完整代码见:

gitee.com/speed_turbo…

总结一下扫码登录的步骤:

  1. 前端请求 → 后台生成二维码和唯一ID
  2. 显示二维码 → 用户打开手机APP扫描
  3. APP解析 → 向服务器报告扫描事件
  4. 用户确认 → APP发送确认登录请求
  5. 前端轮询 → 不断检查登录状态
  6. 获取token → 登录成功,维持会话

下次扫码时,想想背后这场精密的"对话芭蕾",是不是觉得技术真的很神奇呢?

记得点赞关注,下期带你用WebSocket优化这个流程!

我是用故事讲技术的【小枫学幽默】确定不关注一下?