扫码登录实现方案

实现 PC 端扫描二维码登录的完整流程

问题

如何实现扫描二维码登录 PC 网站?

解答

登录原理

二维码登录是一种身份认证方式,需要完成两个核心任务:

  1. 告诉系统我是谁
  2. 向系统证明我是谁

完整流程

第一步:生成二维码

PC 端向服务端请求生成登录二维码:

// PC 端请求生成二维码
async function generateQRCode() {
  const deviceInfo = getDeviceInfo(); // 获取设备信息
  
  const response = await fetch('/api/qrcode/generate', {
    method: 'POST',
    body: JSON.stringify({ deviceInfo })
  });
  
  const { qrcodeId } = await response.json();
  
  // 生成二维码图片
  displayQRCode(qrcodeId);
  
  // 开始轮询二维码状态
  pollQRCodeStatus(qrcodeId);
}

// 轮询二维码状态
function pollQRCodeStatus(qrcodeId) {
  const timer = setInterval(async () => {
    const response = await fetch(`/api/qrcode/status?id=${qrcodeId}`);
    const { status, token } = await response.json();
    
    if (status === 'scanned') {
      updateUI('已扫描,请在手机端确认');
    } else if (status === 'confirmed') {
      clearInterval(timer);
      loginSuccess(token);
    }
  }, 1000);
}

服务端生成二维码 ID 并绑定设备信息:

// 服务端生成二维码
app.post('/api/qrcode/generate', (req, res) => {
  const { deviceInfo } = req.body;
  const qrcodeId = generateUUID();
  
  // 存储二维码信息(Redis 示例)
  redis.setex(`qrcode:${qrcodeId}`, 300, JSON.stringify({
    deviceInfo,
    status: 'waiting',
    createdAt: Date.now()
  }));
  
  res.json({ qrcodeId });
});

第二步:扫描二维码

手机端扫描二维码并发送身份信息:

// 手机端扫描二维码
async function scanQRCode(qrcodeId) {
  const userToken = getLocalToken(); // 手机端已登录的 token
  
  const response = await fetch('/api/qrcode/scan', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${userToken}`
    },
    body: JSON.stringify({ qrcodeId })
  });
  
  const { tempToken } = await response.json();
  
  // 显示确认界面
  showConfirmDialog(tempToken);
}

服务端处理扫描请求:

// 服务端处理扫描
app.post('/api/qrcode/scan', authenticate, (req, res) => {
  const { qrcodeId } = req.body;
  const userId = req.user.id;
  
  // 更新二维码状态
  const qrcodeData = redis.get(`qrcode:${qrcodeId}`);
  qrcodeData.status = 'scanned';
  qrcodeData.userId = userId;
  
  // 生成临时 token
  const tempToken = generateTempToken(qrcodeId, userId);
  
  redis.setex(`qrcode:${qrcodeId}`, 300, JSON.stringify(qrcodeData));
  
  res.json({ tempToken });
});

第三步:确认登录

手机端确认登录:

// 手机端确认登录
async function confirmLogin(tempToken) {
  await fetch('/api/qrcode/confirm', {
    method: 'POST',
    body: JSON.stringify({ tempToken })
  });
  
  showSuccess('登录成功');
}

服务端生成 PC 端登录 token:

// 服务端处理确认
app.post('/api/qrcode/confirm', (req, res) => {
  const { tempToken } = req.body;
  const { qrcodeId, userId } = verifyTempToken(tempToken);
  
  // 获取设备信息
  const qrcodeData = redis.get(`qrcode:${qrcodeId}`);
  
  // 生成 PC 端登录 token
  const loginToken = generateLoginToken(userId, qrcodeData.deviceInfo);
  
  // 更新状态
  qrcodeData.status = 'confirmed';
  qrcodeData.token = loginToken;
  redis.setex(`qrcode:${qrcodeId}`, 60, JSON.stringify(qrcodeData));
  
  res.json({ success: true });
});

使用 WebSocket 优化

替代轮询方案,使用 WebSocket 实时通知:

// PC 端建立 WebSocket 连接
const ws = new WebSocket(`ws://api.example.com/qrcode/${qrcodeId}`);

ws.onmessage = (event) => {
  const { status, token } = JSON.parse(event.data);
  
  if (status === 'scanned') {
    updateUI('已扫描,请在手机端确认');
  } else if (status === 'confirmed') {
    loginSuccess(token);
  }
};

关键点

  • 二维码中包含唯一 ID,服务端通过 ID 绑定设备信息和用户身份
  • 手机端必须是已登录状态,扫码时携带身份凭证
  • 使用临时 token 作为扫描和确认之间的凭证,提高安全性
  • PC 端通过轮询或 WebSocket 实时获取二维码状态变化
  • 二维码应设置过期时间(如 5 分钟),避免安全风险