PHP项目如何对接扫码登录接口?

wen PHP项目 24

本文目录导读:

PHP项目如何对接扫码登录接口?

  1. 扫码登录基本原理
  2. 主流对接方式
  3. 自建扫码登录系统
  4. 前端实现
  5. API接口实现
  6. 安全性建议
  7. 最佳实践

我来详细介绍PHP项目对接扫码登录接口的完整流程。

扫码登录基本原理

用户手机扫码 → 获取二维码信息 → 确认登录 → 服务器验证 → 登录成功

主流对接方式

微信扫码登录

<?php
class WechatScanLogin {
    private $appId;
    private $appSecret;
    public function __construct($appId, $appSecret) {
        $this->appId = $appId;
        $this->appSecret = $appSecret;
    }
    // 获取二维码URL
    public function getQrCodeUrl($state = '') {
        $redirectUri = urlencode('http://yourdomain.com/callback.php');
        $url = "https://open.weixin.qq.com/connect/qrconnect";
        $url .= "?appid=" . $this->appId;
        $url .= "&redirect_uri=" . $redirectUri;
        $url .= "&response_type=code";
        $url .= "&scope=snsapi_login";
        $url .= "&state=" . $state;
        $url .= "#wechat_redirect";
        return $url;
    }
    // 回调处理
    public function callback($code) {
        // 获取access_token
        $tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token";
        $tokenUrl .= "?appid=" . $this->appId;
        $tokenUrl .= "&secret=" . $this->appSecret;
        $tokenUrl .= "&code=" . $code;
        $tokenUrl .= "&grant_type=authorization_code";
        $response = $this->httpGet($tokenUrl);
        $data = json_decode($response, true);
        if (isset($data['access_token'])) {
            // 获取用户信息
            $userInfo = $this->getUserInfo($data['access_token'], $data['openid']);
            return $userInfo;
        }
        return false;
    }
    // 获取用户信息
    private function getUserInfo($accessToken, $openid) {
        $url = "https://api.weixin.qq.com/sns/userinfo";
        $url .= "?access_token=" . $accessToken;
        $url .= "&openid=" . $openid;
        $response = $this->httpGet($url);
        return json_decode($response, true);
    }
    // HTTP GET请求
    private function httpGet($url) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        $response = curl_exec($ch);
        curl_close($ch);
        return $response;
    }
}

支付宝扫码登录

<?php
require_once 'vendor/autoload.php'; // 引入支付宝SDK
use Alipay\EasySDK\Kernel\Factory;
use Alipay\EasySDK\Kernel\Config;
class AlipayScanLogin {
    private $config;
    public function __construct($appId, $privateKey, $alipayPublicKey) {
        $this->config = new Config();
        $this->config->protocol = 'https';
        $this->config->gatewayHost = 'openapi.alipay.com';
        $this->config->signType = 'RSA2';
        $this->config->appId = $appId;
        $this->config->merchantPrivateKey = $privateKey;
        $this->config->alipayPublicKey = $alipayPublicKey;
        Factory::setOptions($this->config);
    }
    // 生成扫码登录页面
    public function getLoginPage() {
        // 生成state参数防止CSRF
        $state = bin2hex(random_bytes(16));
        $_SESSION['alipay_state'] = $state;
        $redirectUri = urlencode('http://yourdomain.com/alipay_callback.php');
        $url = "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm";
        $url .= "?app_id=" . $this->config->appId;
        $url .= "&scope=auth_user";
        $url .= "&redirect_uri=" . $redirectUri;
        $url .= "&state=" . $state;
        return $url;
    }
    // 处理回调
    public function handleCallback($authCode, $state) {
        // 验证state
        if ($state !== $_SESSION['alipay_state']) {
            throw new Exception('Invalid state parameter');
        }
        try {
            // 获取access_token
            $result = Factory::openAuth()->token()->get($authCode);
            $accessToken = $result->accessToken;
            // 获取用户信息
            $userResult = Factory::openAuth()->user()->get();
            return $userResult;
        } catch (Exception $e) {
            error_log('Alipay login error: ' . $e->getMessage());
            return false;
        }
    }
}

自建扫码登录系统

<?php
class CustomScanLogin {
    private $db;
    private $redis;
    public function __construct($db, $redis) {
        $this->db = $db;
        $this->redis = $redis;
    }
    // 生成二维码
    public function generateQRCode() {
        // 生成唯一标识
        $ticket = $this->generateTicket();
        $expireTime = 300; // 5分钟有效期
        // 保存到Redis
        $this->redis->setex("scan_login:{$ticket}", $expireTime, json_encode([
            'status' => 'pending',
            'created_at' => time()
        ]));
        // 生成二维码内容
        $qrContent = json_encode([
            'type' => 'scan_login',
            'ticket' => $ticket,
            'timestamp' => time()
        ]);
        // 使用QR码库生成二维码图片
        $this->generateQRCodeImage($qrContent);
        return [
            'ticket' => $ticket,
            'expire_in' => $expireTime
        ];
    }
    // 扫码验证(手机端调用)
    public function scanQRCode($ticket, $userId) {
        $loginData = $this->redis->get("scan_login:{$ticket}");
        if (!$loginData) {
            return ['error' => '二维码已过期'];
        }
        $loginData = json_decode($loginData, true);
        if ($loginData['status'] !== 'pending') {
            return ['error' => '二维码已被使用'];
        }
        // 生成临时token
        $tempToken = bin2hex(random_bytes(32));
        // 更新状态
        $this->redis->setex("scan_login:{$ticket}", 60, json_encode([
            'status' => 'scanned',
            'user_id' => $userId,
            'temp_token' => $tempToken,
            'created_at' => $loginData['created_at']
        ]));
        return [
            'success' => true,
            'temp_token' => $tempToken
        ];
    }
    // 确认登录(手机端调用)
    public function confirmLogin($ticket, $tempToken) {
        $loginData = $this->redis->get("scan_login:{$ticket}");
        if (!$loginData) {
            return ['error' => '登录已过期'];
        }
        $loginData = json_decode($loginData, true);
        if ($loginData['temp_token'] !== $tempToken) {
            return ['error' => '验证失败'];
        }
        // 生成登录token
        $loginToken = bin2hex(random_bytes(32));
        // 更新状态
        $this->redis->setex("scan_login:{$ticket}", 60, json_encode([
            'status' => 'confirmed',
            'login_token' => $loginToken,
            'user_id' => $loginData['user_id']
        ]));
        return [
            'success' => true,
            'login_token' => $loginToken
        ];
    }
    // PC端轮询检查(轮询)
    public function checkLoginStatus($ticket) {
        $loginData = $this->redis->get("scan_login:{$ticket}");
        if (!$loginData) {
            return ['status' => 'expired'];
        }
        $loginData = json_decode($loginData, true);
        switch ($loginData['status']) {
            case 'pending':
                return ['status' => 'pending'];
            case 'scanned':
                return ['status' => 'scanned'];
            case 'confirmed':
                // 清除Redis中的记录
                $this->redis->del("scan_login:{$ticket}");
                return [
                    'status' => 'confirmed',
                    'login_token' => $loginData['login_token']
                ];
            default:
                return ['status' => 'error'];
        }
    }
    // 生成唯一ticket
    private function generateTicket() {
        return bin2hex(random_bytes(16));
    }
    // 生成二维码图片
    private function generateQRCodeImage($content) {
        // 使用phpqrcode库
        require_once 'phpqrcode/qrlib.php';
        QRcode::png($content, false, QR_ECLEVEL_L, 4);
    }
}

前端实现

<!-- PC端扫码页面 -->
<!DOCTYPE html>
<html>
<head>扫码登录</title>
    <style>
        .qr-container {
            width: 300px;
            margin: 100px auto;
            text-align: center;
        }
        #qr-code {
            width: 200px;
            height: 200px;
        }
        .status-text {
            margin-top: 20px;
            color: #666;
        }
    </style>
</head>
<body>
    <div class="qr-container">
        <h2>扫码登录</h2>
        <div id="qr-code"></div>
        <p class="status-text" id="status-text">请使用手机扫码</p>
        <button onclick="refreshQRCode()" id="refresh-btn">刷新二维码</button>
    </div>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
        let pollTimer;
        let currentTicket;
        // 初始化
        $(document).ready(function() {
            refreshQRCode();
        });
        // 刷新二维码
        function refreshQRCode() {
            $.ajax({
                url: '/api/generate_qr.php',
                method: 'GET',
                success: function(response) {
                    currentTicket = response.ticket;
                    $('#qr-code').html('<img src="api/qr_image.php?ticket=' + response.ticket + '" width="200" height="200">');
                    $('#status-text').text('请使用手机扫码');
                    startPolling();
                },
                error: function() {
                    $('#status-text').text('生成二维码失败');
                }
            });
        }
        // 开始轮询
        function startPolling() {
            if (pollTimer) {
                clearInterval(pollTimer);
            }
            pollTimer = setInterval(function() {
                $.ajax({
                    url: '/api/check_login.php?ticket=' + currentTicket,
                    method: 'GET',
                    success: function(response) {
                        switch(response.status) {
                            case 'pending':
                                $('#status-text').text('请使用手机扫码');
                                break;
                            case 'scanned':
                                $('#status-text').text('请在手机上确认登录');
                                break;
                            case 'confirmed':
                                $('#status-text').text('登录成功!');
                                clearInterval(pollTimer);
                                // 处理登录成功
                                handleLoginSuccess(response.login_token);
                                break;
                            case 'expired':
                                $('#status-text').text('二维码已过期');
                                clearInterval(pollTimer);
                                refreshQRCode();
                                break;
                        }
                    }
                });
            }, 2000); // 每2秒轮询一次
        }
        // 登录成功处理
        function handleLoginSuccess(token) {
            // 存储token到cookie或localStorage
            document.cookie = "login_token=" + token + "; path=/";
            // 跳转到首页
            setTimeout(function() {
                window.location.href = '/dashboard.php';
            }, 1000);
        }
    </script>
</body>
</html>

API接口实现

<?php
// generate_qr.php - 生成二维码接口
header('Content-Type: application/json');
require_once 'config.php';
require_once 'ScanLogin.php';
$scanLogin = new CustomScanLogin($db, $redis);
$result = $scanLogin->generateQRCode();
echo json_encode($result);
// qr_image.php - 二维码图片接口
$ticket = $_GET['ticket'];
// 生成并返回二维码图片
// ...
// check_login.php - 检查登录状态接口
$ticket = $_GET['ticket'];
$scanLogin = new CustomScanLogin($db, $redis);
$result = $scanLogin->checkLoginStatus($ticket);
echo json_encode($result);

安全性建议

防止CSRF攻击

// 生成和验证state参数
$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;

Token安全存储

// 使用HttpOnly Cookie存储token
setcookie('login_token', $token, [
    'expires' => time() + 3600,
    'path' => '/',
    'domain' => 'yourdomain.com',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Strict'
]);

请求频率限制

// 使用Redis限制轮询频率
$ip = $_SERVER['REMOTE_ADDR'];
$key = "rate_limit:{$ip}:scan_login";
$count = $redis->incr($key);
if ($count > 10) {
    $redis->expire($key, 60);
    die(json_encode(['error' => '请求过于频繁']));
}
$redis->expire($key, 60);

最佳实践

  1. 二维码有效期:建议5分钟内有效
  2. 轮询间隔:建议2-3秒轮询一次
  3. 错误处理:网络异常时自动重试
  4. 用户体验:显示扫码状态变化动画
  5. 安全防护:添加验证码防止暴力破解

这个实现方案可以满足大多数扫码登录场景的需求,你可以根据具体业务需求进行调整。

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