本文目录导读:

我来详细介绍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);
最佳实践
- 二维码有效期:建议5分钟内有效
- 轮询间隔:建议2-3秒轮询一次
- 错误处理:网络异常时自动重试
- 用户体验:显示扫码状态变化动画
- 安全防护:添加验证码防止暴力破解
这个实现方案可以满足大多数扫码登录场景的需求,你可以根据具体业务需求进行调整。