一、引言
在当今Web应用中,快捷登录已成为提升用户体验的关键功能。微信作为国内用户量最大的社交平台之一,其扫码登录功能被广泛应用于各类网站和应用中。本文将详细解析一个基于WebSocket实时通信的微信扫码登录系统,该系统通过PHP+JavaScript实现,结合微信OAuth2.0授权机制与WebSocket实时消息传递,实现了"PC端展示二维码-手机微信扫码-实时同步登录状态"的完整流程。
二、功能概述
本系统核心目标是实现"微信扫码快速登录",主要包含以下功能:
- 登录页功能:生成唯一客户端ID,建立WebSocket连接,展示包含客户端ID的登录二维码
- 扫码页功能:用户扫码后获取微信OpenID,通过WebSocket将OpenID发送至登录页
- 实时通信:基于WebSocket实现登录页与扫码页的双向消息传递,实时同步扫码状态、验证结果
- OpenID验证:检查扫码用户的OpenID是否已注册,完成登录状态确认
- 状态同步:登录成功/失败状态实时反馈到登录页,完成登录流程闭环
三、核心技术栈
本系统融合了多种前后端技术,核心技术栈如下:
- 后端语言:PHP(处理参数验证、微信OpenID获取、会话管理)
- 前端技术:HTML5 + JavaScript(页面渲染、WebSocket客户端、二维码生成)
- 实时通信:WebSocket(实现登录页与扫码页的实时消息交互)
- 微信授权:微信OAuth2.0(通过授权流程获取用户OpenID)
- 二维码生成:QR_1.js(前端生成包含登录信息的二维码)
- 异步请求:Ajax(检查OpenID是否已注册)
四、程序结构解析
4.1 PHP核心处理部分(后端逻辑)
PHP部分主要负责参数校验、状态区分及微信OpenID获取,代码位于文件开头和结尾:
// 参数校验与状态定义
(!isset($_GET['Action'])) && exit('缺少必要参数 Action');
$Action = @$_GET['Action'];
$IsLogin = ($Action == 'Login'); // 登录页标识
$IsScan = ($Action == 'Scan'); // 扫码页标识
(!$IsLogin && !$IsScan) && exit("无效的 Action 参数 $Action");
// 扫码时获取微信OpenID和客户端ID
$OpenID = $IsScan? GetOpenID()['openid'] : '';
$Client_ID = $IsScan? $_GET['client_id'] : '';
核心函数GetOpenID()实现了微信OAuth2.0授权流程,主要步骤包括:
- 配置微信参数:加载app_id、app_secret等微信开放平台配置
- 会话管理:使用PHP Session保存授权状态(state)与OpenID结果,避免重复授权
- 授权流程:
- 若未获取code,生成随机state并跳转至微信授权页
- 授权回调后验证state合法性,防止CSRF攻击
- 通过code调用微信接口获取access_token与OpenID
- 结果缓存:将获取到的OpenID及相关信息存入Session,避免重复请求微信接口
4.2 HTML页面结构(前端展示)
HTML部分根据Action参数动态生成页面内容,区分登录页与扫码页:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $Action; ?></title>
<?php if ($IsLogin): ?>
<script src="js/QR_1.js"></script> <!-- 登录页加载二维码生成库 -->
<style>/* 二维码样式 */</style>
<?php endif; ?>
</head>
<body>
<h1><?php echo $Action; ?></h1>
<div id="QR_Code" style="width: 200px; height: 200px;"></div> <!-- 二维码容器 -->
<p><?php echo $IsLogin? '请使用您的手机扫描二维码以授权登录。' : ''; ?></p>
<div id="ZT_Text"></div> <!-- 状态提示容器 -->
<p id="OpenID"><?=$OpenID?></p> <!-- OpenID展示容器 -->
</body>
</html>
- 登录页(
Action=Login):加载二维码生成库,显示二维码容器和登录提示 - 扫码页(
Action=Scan):隐藏二维码相关元素,展示获取到的OpenID
4.3 JavaScript逻辑(核心交互)
JavaScript部分是系统的"神经中枢",负责WebSocket通信、二维码生成、消息处理等核心逻辑,主要包含以下模块:
4.3.1 基础变量定义
// 状态标识(从PHP获取)
let IsLogin = <?php echo $IsLogin? 'true' : 'false'; ?>;
let IsScan = <?php echo $IsScan? 'true' : 'false'; ?>;
// 生成随机字符串(用于客户端ID)
function XL_RandomStr(Len=32) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return Array.from({ length: Len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}
// 客户端ID(唯一标识当前页面实例)
let Client_ID = 'Login_' + (IsScan? 'Scan_' : '') + Date.now() + XL_RandomStr(16);
let Socket = null; // WebSocket实例
let ServerUrl = `wss://www.0554h.com:9090?client_id=${Client_ID}&auth=1232131321`; // WebSocket服务地址
let To_Client_ID = IsScan? '<?php echo $Client_ID; ?>' : ''; // 目标客户端ID(扫码页指向登录页)
4.3.2 WebSocket通信模块
WebSocket是实现实时通信的核心,包含连接建立、消息发送、消息接收、连接关闭等功能:
// 建立WebSocket连接
function WebSocket_Open() {
if (Socket && Socket.readyState === WebSocket.OPEN) return;
Socket = new WebSocket(ServerUrl);
// 连接成功回调
Socket.onopen = () => {
if (IsLogin) {
ZTElement.innerHTML = '连接成功 客户端ID: ' + Client_ID + ' 等待微信扫码登录~!';
ShowQRcode(); // 登录页生成二维码
}else if (IsScan) {
// 扫码页发送"正在扫码"消息
const Send_OpenID = {
Type: 'Login',
Action: 'Scanning',
From: Client_ID,
To: To_Client_ID
};
sendMessage(Send_OpenID);
}
};
// 错误处理
Socket.onerror = (error) => { ZTElement.innerHTML = '<span style="color: red;">连接错误: ' + error.message + '</span>'; };
// 关闭处理
Socket.onclose = (event) => { ZTElement.innerHTML = '<span style="color: red;">连接关闭</span>'; };
// 消息接收处理
Socket.onmessage = (event) => {
if (!/^\{.*\}$/.test(event.data)) return; // 验证JSON格式
const Msg = JSON.parse(event.data);
if (Msg.Type === 'Login') {
// 根据消息Action处理不同逻辑(扫码中、OpenID发送、注册状态等)
switch (Msg.Action) {
case 'Send_OpenID': /* 处理OpenID接收与验证 */ break;
case 'Scanning': /* 显示扫码中状态 */ break;
case 'UnRegister': /* 显示未注册状态 */ break;
case 'Register': /* 显示已注册状态 */ break;
}
}
};
}
// 发送消息
function sendMessage(SendContent) {
if (!Socket || Socket.readyState !== WebSocket.OPEN) return;
const messageStr = JSON.stringify(SendContent);
try {
Socket.send(messageStr);
} catch (error) {
ZTElement.innerHTML = '<span style="color: red;">发送失败: ' + error.message + '</span>';
}
}
// 关闭连接
function WebSocket_Close() {
if (!Socket || Socket.readyState !== WebSocket.OPEN) return;
Socket.close(1000, '客户端主动断开连接');
}
4.3.3 二维码生成模块
登录页通过ShowQRcode()函数生成包含客户端ID的二维码,供微信扫码:
function ShowQRcode() {
QR_Code.style.display = 'block';
try {
if (QRCodeElement && typeof qrcode !== 'undefined') {
var QR = qrcode(0, 'H'); // 高纠错级别
// 二维码内容:扫码页地址+当前客户端ID(确保扫码后能定位到当前登录页)
QR.addData(`http://0554h.com/API/Socket/Socket_Login.php?Action=Scan&client_id=${Client_ID}`);
QR.make();
var moduleCount = QR.getModuleCount();
var margin = 0;
var cellSize = (200 - 2 * margin) / moduleCount; // 自适应200px容器
var imgTag = QR.createImgTag(cellSize, margin);
QRCodeElement.innerHTML = imgTag;
} else {
console.error('二维码功能不可用 - 请检查js/QR_1.js是否正确加载');
}
} catch (error) {
console.error('生成二维码失败:', error);
}
}
4.3.4 OpenID验证与登录模块
通过Ajax检查OpenID是否已注册,并完成登录流程:
// 检查OpenID是否注册
function IsRegister($OpenID) {
if (empty($OpenID)) return false;
// 同步Ajax请求(确保验证完成后再继续)
$.ajax({
url: 'http://0554h.com/API/DB/DB_Login.php',
type: 'POST',
data: { Action: 'Check_Register', OpenID: $OpenID },
async: false, // 同步请求(注意:同步请求可能阻塞UI,实际应用可优化为异步+回调)
success: function(response) {
return response.success;
}
});
}
// 登录逻辑(简化版,实际应用需添加Session/Cookie设置等)
function Login(OpenID) {
// 此处可添加登录状态保存逻辑(如设置Session、生成Token等)
}
4.3.5 页面初始化
页面加载完成后初始化DOM元素,建立WebSocket连接,并启动扫码页的OpenID监测:
document.addEventListener('DOMContentLoaded', function() {
// 获取DOM元素
QRCodeElement = document.getElementById('QR_Code');
ZTElement = document.getElementById('ZT_Text');
OpenID = document.getElementById('OpenID').innerHTML;
// 建立WebSocket连接
WebSocket_Open();
// 扫码页:实时监测OpenID并发送给登录页
if(IsScan) {
setInterval(() => {
if(OpenID != '' && OpenID2 != OpenID && Socket.readyState === WebSocket.OPEN) {
const Send_OpenID = {
Type: 'Login',
Action: 'Send_OpenID',
From: Client_ID,
To: To_Client_ID,
OpenID: OpenID
};
sendMessage(Send_OpenID);
OpenID2 = OpenID;
}
}, 500);
}
});
五、完整工作流程
-
用户访问登录页(Action=Login):
- PHP验证Action参数,标记
IsLogin=true - 前端生成唯一
Client_ID,建立WebSocket连接 - 连接成功后生成二维码(内容为扫码页地址+当前
Client_ID) - 页面显示"等待微信扫码登录"状态
- PHP验证Action参数,标记
-
用户使用微信扫码:
- 微信扫码后跳转到二维码中的扫码页(Action=Scan&client_id=xxx)
- 扫码页PHP调用
GetOpenID()函数,通过微信OAuth2.0流程获取用户OpenID - 扫码页生成自己的
Client_ID,并通过To_Client_ID记录登录页的Client_ID - 扫码页建立WebSocket连接,发送"Scanning"消息到登录页,登录页显示"xxx正在扫码中"
-
OpenID验证与登录:
- 扫码页通过定时器监测到OpenID已获取,发送
Send_OpenID消息(包含OpenID)到登录页 - 登录页接收OpenID,调用
IsRegister()通过Ajax检查是否注册 - 若未注册:发送
UnRegister消息,登录页显示"OpenID未注册" - 若已注册:发送
Register消息,调用Login()完成登录,发送LoginSuccess消息,隐藏二维码并显示"登录成功",最后关闭WebSocket连接
- 扫码页通过定时器监测到OpenID已获取,发送
六、关键技术点解析
-
客户端身份标识(Client_ID):
- 由时间戳+随机字符串组成,确保唯一性
- 登录页与扫码页通过
Client_ID和To_Client_ID建立一对一通信关系 - 避免多用户同时登录时的消息混淆
-
微信OpenID获取流程:
- 基于微信OAuth2.0的授权码模式(code模式)
- 通过
state参数防止CSRF攻击(服务器端验证state合法性) - 支持结果缓存(Session存储),减少重复授权请求
-
WebSocket消息协议:
- 消息格式为JSON,包含
Type(类型)、Action(动作)、From(发送方)、To(接收方)等字段 - 基于
Type=Login的消息类型,通过Action区分不同操作(扫码中、发送OpenID、验证结果等) - 确保消息仅在指定的客户端间传递(通过
To字段校验)
- 消息格式为JSON,包含
七、注意事项与优化建议
-
配置安全:
- 微信
app_id和app_secret需替换为实际值,建议通过环境变量(getenv)加载,避免硬编码 - WebSocket连接的
auth参数(示例中为1232131321)需替换为实际的身份验证逻辑,防止未授权连接
- 微信
-
WebSocket服务器:
- 需确保WebSocket服务器(wss://www.0554h.com:9090)正常运行并支持跨域请求
- 生产环境需配置SSL证书(wss协议),确保通信安全
-
异步处理优化:
IsRegister()函数使用了同步Ajax(async: false),可能导致UI阻塞,建议改为异步请求+回调函数- 可添加请求超时处理,避免因后端响应缓慢导致的页面卡死
-
错误处理增强:
- 增加WebSocket重连机制(如连接断开后自动重试)
- 对微信授权失败、二维码生成失败等场景添加更友好的提示
-
安全性提升:
- 客户端ID可添加签名机制,防止伪造
- OpenID等敏感信息在传输和存储时建议加密
- 增加二维码有效期限制,超时后自动刷新
八、总结
本系统通过PHP+JavaScript+WebSocket技术栈,结合微信OAuth2.0授权机制,实现了一套完整的微信扫码登录方案。其核心优势在于通过WebSocket实现了登录状态的实时同步,避免了传统轮询方式的性能损耗,同时借助微信生态实现了"免输入密码"的快捷登录体验。
该系统可应用于各类需要微信快捷登录的Web应用,如管理后台、社区论坛、电商平台等。通过本文的解析,开发者可快速理解扫码登录的实现原理,并根据实际需求进行扩展和优化。
源代码
(!isset($_GET['Action'])) && exit('缺少必要参数 Action'); // 缺少必要参数 Action
$Action = @$_GET['Action']; // 获取 Action 参数
$IsLogin = ($Action == 'Login'); // 是否登录
$IsScan = ($Action == 'Scan'); // 是否扫描
(!$IsLogin && !$IsScan) && exit("无效的 Action 参数 $Action"); // 无效的 Action 参数
// 扫描时获取微信OpenID
$OpenID = $IsScan? GetOpenID()['openid'] : '';
// 扫描时获取登录页面客户端ID
$Client_ID = $IsScan? $_GET['client_id'] : '';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $Action; ?></title>
<?php if ($IsLogin): ?>
<script src="js/QR_1.js"></script>
<style>
#QR_Code {
margin: 20px auto;
/* 默认隐藏 */
display: none;
}
</style>
<?php endif; ?>
</head>
<body>
<h1><?php echo $Action; ?></h1>
<div id="QR_Code" style="width: 200px; height: 200px;"></div>
<p><?php echo $IsLogin? '请使用您的手机扫描二维码以授权登录。' : ''; ?></p>
<div id="ZT_Text"></div>
<p id="OpenID"><?=$OpenID?></p>
<script>
// 登录状态
let IsLogin = <?php echo $IsLogin? 'true' : 'false'; ?>;
// 扫描状态
let IsScan = <?php echo $IsScan? 'true' : 'false'; ?>;
// 生成随机字符串 RandomStr 长度为 Len
function XL_RandomStr(Len=32) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return Array.from({ length: Len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}
let OpenID2 = XL_RandomStr();
// 客户端ID
let Client_ID = 'Login_' + (IsScan? 'Scan_' : '') + Date.now() + XL_RandomStr(16);
// 定义DOM元素变量
let QRCodeElement, ZTElement, OpenID;
// 其他全局变量
let Socket = null;
let ServerUrl = `wss://www.0554h.com:9090?client_id=${Client_ID}&auth=1232131321`;
// 登录页面客户端ID
let To_Client_ID = IsScan? '<?php echo $Client_ID; ?>' : '';
function WebSocket_Open() {
if (Socket && Socket.readyState === WebSocket.OPEN) return;
Socket = new WebSocket(ServerUrl);
// ------------------------------------------------------------------------------------
// 连接成功
Socket.onopen = () => {
if (IsLogin) {
ZTElement.innerHTML = '连接成功 客户端ID: ' + Client_ID + ' 等待微信扫码登录~!';
// 生成二维码
ShowQRcode();
}else if (IsScan) {
// 发送OpenID消息内容
const Send_OpenID = {
Type: 'Login',
Action: 'Scanning',
From: Client_ID,
To: To_Client_ID
};
// 发送OpenID
sendMessage(Send_OpenID);
};
};
// ------------------------------------------------------------------------------------
// 连接错误
Socket.onerror = (error) => { ZTElement.innerHTML = '<span style="color: red;">连接错误: ' + error.message + '</span>'; };
// ------------------------------------------------------------------------------------
// 连接关闭
Socket.onclose = (event) => { ZTElement.innerHTML = '<span style="color: red;">连接关闭</span>'; };
// ------------------------------------------------------------------------------------
// 接收消息
Socket.onmessage = (event) => {
//检查消息格式是否为JSON
if (!/^\{.*\}$/.test(event.data)) return;
//解析消息
const Msg = JSON.parse(event.data);
// 处理不同类型的消息
if (Msg.Type === 'Login') {
switch (Msg.Action) {
case 'Send_OpenID':
// 检查是否是目标客户端
if (Msg.To !== Client_ID) return;
// 状态提示
ZTElement.innerHTML = '接收到OpenID: ' + Msg.OpenID;
// 检查OpenID是否注册
if (IsRegister(Msg.OpenID)) {
ZTElement.innerHTML = '<span style="color: red;">OpenID未注册</span>';
// 发送未注册消息内容
const Send_UnRegister = {
Type: 'Login',
Action: 'UnRegister',
From: Client_ID,
To: To_Client_ID,
OpenID: Msg.OpenID
};
// 发送未注册消息
sendMessage(Send_UnRegister);
break;
}else{
ZTElement.innerHTML = '<span style="color: green;">OpenID已注册</span>';
// 发送已注册消息内容
const Send_Register = {
Type: 'Login',
Action: 'Register',
From: Client_ID,
To: To_Client_ID,
OpenID: Msg.OpenID
};
// 发送已注册消息
sendMessage(Send_Register);
// 登录
Login(Msg.OpenID);
// 发送登录成功消息
const Send_LoginSuccess = {
Type: 'Login',
Action: 'LoginSuccess',
From: Client_ID,
To: To_Client_ID,
OpenID: Msg.OpenID
};
// 发送登录成功消息
sendMessage(Send_LoginSuccess);
// 登录成功
IsLogin = true;
// 隐藏二维码
QR_Code.style.display = 'none';
// 状态提示
ZTElement.innerHTML = '登录成功 客户端ID: ' + Client_ID ;
// 关闭连接
WebSocket_Close();
break;
}
break;
case 'Scanning':
ZTElement.innerHTML = Msg.From + ' 正在扫码中...';
break;
case 'UnRegister':
ZTElement.innerHTML = '<span style="color: red;">OpenID未注册</span>';
break;
case 'Register':
ZTElement.innerHTML = '<span style="color: green;">OpenID已注册</span>';
break;
}
}
};
}
// ------------------------------------------------------------------------------------
// 发送消息
function sendMessage(SendContent) {
if (!Socket || Socket.readyState !== WebSocket.OPEN) return;
const messageStr = JSON.stringify(SendContent);
try {
Socket.send(messageStr);
} catch (error) {
ZTElement.innerHTML = '<span style="color: red;">发送失败: ' + error.message + '</span>';
}
}
// ------------------------------------------------------------------------------------
// 断开WebSocket连接
function WebSocket_Close() {
if (!Socket || Socket.readyState !== WebSocket.OPEN) return;
Socket.close(1000, '客户端主动断开连接');
ZTElement.innerHTML = '<span style="color: red;">断开连接成功</span>';
}
// 显示二维码
function ShowQRcode() {
QR_Code.style.display = 'block';
try {
if (QRCodeElement && typeof qrcode !== 'undefined') {
var QR = qrcode(0, 'H'); // 自动检测类型,高纠错级别
QR.addData(`http://0554h.com/API/Socket/Socket_Login.php?Action=Scan&client_id=${Client_ID}`);
QR.make(); // 生成二维码
var moduleCount = QR.getModuleCount(); // 获取模块数量
var margin = 0; // 二维码外边距
var cellSize = (200 - 2 * margin) / moduleCount; // 计算单元格大小以适应200px容器
var imgTag = QR.createImgTag(cellSize, margin); // 创建img标签
QRCodeElement.innerHTML = imgTag; // 插入二维码图片
} else {
console.error((!QRCodeElement)? '未找到二维码元素' : '二维码功能不可用 - 请检查js/QR_1.js是否正确加载');
}
} catch (error) {
console.error('生成二维码失败:', error);
}
}
// 检查OpenID是否注册
function IsRegister($OpenID) {
// 检查OpenID是否存在
if (empty($OpenID)) {
return false;
}
// 使用Ajax检查OpenID是否注册
$.ajax({
url: 'http://0554h.com/API/DB/DB_Login.php',
type: 'POST',
data: { Action: 'Check_Register', OpenID: $OpenID },
async: false,
success: function(response) {
return response.success;
}
});
}
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 获取DOM元素
QRCodeElement = document.getElementById('QR_Code');
ZTElement = document.getElementById('ZT_Text');
OpenID = document.getElementById('OpenID').innerHTML;
// 打开WebSocket连接
WebSocket_Open();
// 实时监测OpenID已获取到数据
if(IsScan) {
setInterval(() => {
if(OpenID != '' && OpenID2 != OpenID && Socket.readyState === WebSocket.OPEN) {
// 发送OpenID消息内容
const Send_OpenID = { Type: 'Login', Action: 'Send_OpenID', From: Client_ID, To: To_Client_ID, OpenID: OpenID };
// 发送OpenID
sendMessage(Send_OpenID);
OpenID2 = OpenID;
}
}, 500);
}
});
</script>
</body>
</html>
<?php
// 获取微信OpenID
function GetOpenID(bool $forceRefresh = false)
{
// ===== 配置区域:在正式环境中请将以下占位值替换为真实的微信开放平台参数 =====
$config = [
'app_id' => getenv('WECHAT_APP_ID') ?: 'wx194*************',
'app_secret' => getenv('WECHAT_APP_SECRET') ?: '8c40048*************************',
'scope' => 'snsapi_base', // 可根据业务需要改为 snsapi_base snsapi_userinfo
];
// ======================================================================
if (empty($config['app_id'])) {
return [
'success' => false,
'message' => '微信 AppID 未配置,请在 GetOpenID 函数中提供有效的 app_id。',
];
}
if (empty($config['app_secret'])) {
return [
'success' => false,
'message' => '微信 AppSecret 未配置,请在 GetOpenID 函数中提供有效的 app_secret。',
];
}
// Session 用于保存 state 与结果
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
$sessionKeyPayload = 'wechat_openid_payload';
$sessionKeyState = 'wechat_oauth_state';
if (!$forceRefresh && isset($_SESSION[$sessionKeyPayload]) && is_array($_SESSION[$sessionKeyPayload])) {
return array_merge(['success' => true, 'cached' => true], $_SESSION[$sessionKeyPayload]);
}
$code = isset($_GET['code']) ? trim($_GET['code']) : '';
$state = isset($_GET['state']) ? trim($_GET['state']) : '';
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$currentUrl = $scheme . '://' . $host . $uri;
if ($code === '') {
if (function_exists('random_bytes')) {
$stateValue = bin2hex(random_bytes(16));
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$stateValue = bin2hex(openssl_random_pseudo_bytes(16));
} else {
$stateValue = bin2hex(str_pad((string)mt_rand(), 16, '0', STR_PAD_LEFT));
}
$_SESSION[$sessionKeyState] = $stateValue;
$authParams = [
'appid' => $config['app_id'],
'redirect_uri' => $currentUrl,
'response_type' => 'code',
'scope' => $config['scope'],
'state' => $stateValue,
];
$authUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize?' . http_build_query($authParams, '', '&', PHP_QUERY_RFC3986) . '#wechat_redirect';
if (!headers_sent()) {
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Location: ' . $authUrl);
} else {
echo '<script>window.location.href=' . json_encode($authUrl, JSON_UNESCAPED_SLASHES) . ';</script>';
}
exit;
}
if (!isset($_SESSION[$sessionKeyState]) || !hash_equals($_SESSION[$sessionKeyState], $state)) {
unset($_SESSION[$sessionKeyState]);
return [
'success' => false,
'message' => 'state 校验失败,可能存在 CSRF 风险或会话已过期。',
'code' => $code,
];
}
unset($_SESSION[$sessionKeyState]);
$tokenUrl = sprintf(
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code',
urlencode($config['app_id']),
urlencode($config['app_secret']),
urlencode($code)
);
$httpGet = function (string $url, int $timeout = 10): array {
if (function_exists('curl_init')) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($ch);
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno !== 0) {
return ['success' => false, 'message' => $error ?: 'cURL 请求失败'];
}
return ['success' => true, 'body' => (string)$body];
}
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'header' => "User-Agent: WeChatOAuthClient/1.0\r\n",
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$body = @file_get_contents($url, false, $context);
if ($body === false) {
$error = error_get_last();
return ['success' => false, 'message' => $error['message'] ?? 'HTTP 请求失败'];
}
return ['success' => true, 'body' => (string)$body];
};
$tokenResponse = $httpGet($tokenUrl);
if ($tokenResponse['success'] === false) {
return [
'success' => false,
'message' => '访问微信接口失败:' . $tokenResponse['message'],
];
}
$tokenData = json_decode($tokenResponse['body'], true);
if (!is_array($tokenData)) {
return [
'success' => false,
'message' => '解析微信返回数据失败。',
'raw' => $tokenResponse['body'],
];
}
if (isset($tokenData['errcode']) && $tokenData['errcode'] !== 0) {
return [
'success' => false,
'message' => '微信接口错误:' . ($tokenData['errmsg'] ?? '未知错误'),
'code' => $tokenData['errcode'],
];
}
if (empty($tokenData['openid'])) {
return [
'success' => false,
'message' => '微信返回数据中缺少 OpenID。',
'data' => $tokenData,
];
}
$result = [
'success' => true,
'cached' => false,
'openid' => $tokenData['openid'],
'access_token' => $tokenData['access_token'] ?? null,
'refresh_token' => $tokenData['refresh_token'] ?? null,
'expires_in' => $tokenData['expires_in'] ?? null,
'scope' => $tokenData['scope'] ?? null,
'unionid' => $tokenData['unionid'] ?? null,
];
$_SESSION[$sessionKeyPayload] = $result;
return $result;
} 


李枭龙10 个月前
AI生成文章:请以上所有知识进行深入分析,确定主要知识点,为每个知识点撰写详细说明并附上具有代表性且带有清晰注释的代码示例,接着根据内容拟定一个准确反映文档核心的标题,最后严格按照 Markdown 格式进行排版,确保文档规范美观,以满足初学者学习使用的需求。
李枭龙1 年前
X Lucas