PHP 扫码登录回调怎么弄

wen PHP项目 5

PHP扫码登录回调全解析:从二维码生成到状态轮询的实战指南


目录导读

  1. 扫码登录的底层逻辑:为什么需要“回调”?
  2. 环境准备与核心组件选型(PHP 7.4+/8.x)
  3. 第一步:生成二维码并绑定唯一的登录票据(Ticket)
  4. 第二步:处理扫码后的回调请求(核心代码与签名校验)
  5. 第三步:前端轮询 vs WebSocket——如何高效获取登录状态
  6. 高频问题解答(Q&A):回调超时、重复回调、跨域与安全
  7. 性能优化与SEO友好型URL设计

扫码登录的底层逻辑:为什么需要“回调”?

扫码登录的本质是“手机确认,PC授权”,用户用手机扫描PC屏幕上的二维码后,手机端完成身份验证,服务器需要通知PC端“登录成功”,这个“通知”动作在HTTP协议下通常有两种实现:

PHP 扫码登录回调怎么弄

  • 服务端主动推送(WebSocket,复杂但实时);
  • 客户端轮询(PC前端每隔2-3秒请求一次“查询状态”接口,简单可靠)。

PHP在多数场景下采用轮询+回调模式,这里的“回调”并非指微信或支付宝的“回调URL”,而是指手机端扫码后,调用你的API接口来标记该二维码已确认,同时PC端通过轮询“回调结果”来获取最终登录态。

关键点:二维码本身不存用户信息,只存一个唯一凭证(Ticket),有效期通常为120秒,所有状态(未扫、已扫待确认、已确认)都冗余在Redis或数据库。


环境准备与核心组件选件(PHP 7.4+/8.x)

  • PHP框架:Laravel(推荐)或ThinkPHP,自带CSRF与路由过滤;
  • 缓存/存储:Redis(推荐用于Ticket状态机)或MySQL(备选);
  • 二维码生成endroid/qr-code(Composer包);
  • HTTP客户端Guzzle(用于若需调用第三方登录API,如微信开放平台)。

目录结构建议

/app/Http/Controllers/Auth/ScanLoginController.php
/app/Services/ScanLoginService.php
/routes/web.php
/resources/views/scan.blade.php

第一步:生成二维码并绑定唯一的登录票据(Ticket)

核心流程:

  1. 生成ticket = md5(uniqid(mt_rand(), true))
  2. ticket存入Redis,设置status=0(待扫描),expired_at = now()+120s
  3. ticket作为参数,生成二维码图片URL。

代码示例(Laravel)

public function showQrCode() 
{
    $ticket = md5(uniqid(mt_rand(), true));
    Redis::setex('scan:ticket:'.$ticket, 120, json_encode([
        'status' => 0, // 0未扫,1已扫,2已确认
        'user_id' => null
    ]));
    $qrCode = QrCode::size(300)->generate(route('scan.confirm', ['ticket' => $ticket]));
    return view('scan', compact('ticket', 'qrCode'));
}

重要:二维码内容指向的scan.confirm路由,即手机端点击确认后访问的“回调”URL


第二步:处理扫码后的回调请求(核心代码与签名校验)

这是“回调”的核心,手机扫码后,会跳转到以下URL:

GET /scan/confirm?ticket=xxx&user_id=123&sign=sha256(...)

安全设计

  • 必须校验sign签名(HMAC-SHA256,密钥在服务端);
  • 必须校验ticket存在且未过期;
  • 回调成功后,更新Redis状态为1(已扫待确认),并记录user_id
public function confirm(Request $request)
{
    $ticket = $request->input('ticket');
    $userId = $request->input('user_id');
    $sign = $request->input('sign');
    // 1. 验证签名(防止伪造)
    $secretKey = config('app.scan_secret');
    $expectedSign = hash_hmac('sha256', $ticket.$userId, $secretKey);
    if (!hash_equals($expectedSign, $sign)) {
        return response()->json(['code' => 403, 'msg' => '非法请求']);
    }
    // 2. 检查Redis状态
    $cacheKey = 'scan:ticket:'.$ticket;
    $ticketData = Redis::get($cacheKey);
    if (!$ticketData || json_decode($ticketData, true)['status'] !== 0) {
        return response()->json(['code' => 400, 'msg' => '二维码失效或已被使用']);
    }
    // 3. 更新状态为“已确认”(此时PC端轮询会拿到user_id)
    Redis::setex($cacheKey, 60, json_encode([
        'status' => 2,
        'user_id' => $userId
    ]));
    return response()->json(['code' => 200, 'msg' => '登录确认成功,请回到电脑端完成登录']);
}

注意:这里的“回调”是手机端请求的,与PC端的轮询接口分离,PC前端的轮询接口是/scan/status/{ticket},仅负责读取Redis状态。


第三步:前端轮询 vs WebSocket——如何高效获取登录状态

PC端页面在展示二维码后,立即启动setInterval,每2秒请求/scan/status/{ticket}

轮询接口(PHP)

public function status($ticket)
{
    $data = Redis::get('scan:ticket:'.$ticket);
    if (!$data) {
        return response()->json(['code' => 404, 'msg' => 'ticket过期']);
    }
    $data = json_decode($data, true);
    if ($data['status'] == 2) {
        // 登录成功,可签发JWT或写入Session
        Auth::loginUsingId($data['user_id']);
        return response()->json(['code' => 200, 'status' => 'confirmed', 'redirect' => '/dashboard']);
    }
    return response()->json(['code' => 200, 'status' => $data['status'] == 0 ? 'unscanned' : 'scanned']);
}

性能优化

  • 建议前端使用指数退避策略(如1s,2s,4s,最大10s),减少无效请求;
  • 若追求极致体验,可集成Laravel Echo + Pusher进行WebSocket推送,但PHP原生下轮询已足够支撑中等规模应用。

高频问题解答(Q&A)

Q1:回调时发现Ticket已过期怎么办? A:在confirm方法中捕获Redis::get返回null,此时应返回code:400,并提示“二维码已过期,请刷新重试”,前端在轮询中收到此状态,应停止轮询并重新生成二维码。

Q2:用户重复点击“确认登录”导致状态冲突? A:在confirm方法中,若发现status !== 0(即已扫或已确认),直接返回“重复操作”,不要覆盖user_id,PC端的轮询只认第一次写入的user_id

Q3:如何防止CSRF攻击? A:回调URL(/scan/confirm)必须使用POST方法(或GET需加签名),使用Laravel的VerifyCsrfToken中间件时,需将scan/confirm排除,但建议保留签名验证,双重保障,推荐将confirm设成POST,前端二维码生成时用一个scan_token参数,并在确认页使用AJAX提交。

Q4:回调地址是否需要暴露公网? A:不需要,手机端扫码后,实际上跳转的还是你的PC端页面(手机浏览器打开),然后手机端浏览器再向你的PHP后端发起确认请求,不存在外网直接访问内网回调的问题。

Q5:如何做多端(PC+移动Web)统一登录态? A:确认成功后,在status方法中签发JWT,前端将JWT存储于localStorage,后续请求头携带Authorization: Bearer,这样PC和手机Web共用一套Token体系。


性能优化与SEO友好型URL设计

  • Redis过期时间:建议ticket有效期为120秒,确认状态保留60秒,防止脏数据;
  • Nginx配置:对/scan/status/{ticket}设置add_header Cache-Control "no-store",避免浏览器缓存;
  • URL设计:二维码内容应使用短链(如yourdomain.com/s/{ticket}),SEO无影响,但便于在微信中打开,使用s短路由,301跳转到/scan/confirm,减少二维码复杂度。
  • 数据库索引:若使用MySQL存储Ticket,需给ticket字段加唯一索引,但Redis方案更优。

本文从二维码生成、签名回调、状态轮询到安全防御,完整拆解了PHP实现扫码登录的每一步,核心原则是:Ticket的生命周期管理回调接口的幂等性,实际项目中,请务必结合业务场景调整超时时间与安全策略,如果你正在开发类似功能,建议先用Redis调试单机流程,再横向扩展服务,祝你一次通过!

抱歉,评论功能暂时关闭!