ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

PHP微信扫码后端实现:生成带参二维码与回调验签解密

PHP微信扫码后端实现:生成带参二维码与回调验签解密 简介本资源是一套基于微信JS-SDK实现网页端‘扫一扫’功能的轻量级开发示例面向PHP后端开发者及微信公众号应用开发者解决在自定义网页中调用微信原生扫码能力并处理返回结果的核心需求。压缩包共4个文件含2个PHP脚本jssdk.php用于签名生成与SDK注入sample.php为调用入口、2个JSON配置文件access_token.json与jsapi_ticket.json用于缓存微信平台凭证总大小仅3KB结构精简、即拿即用。已有96人学习下载适合快速集成扫码功能、理解微信JS-SDK鉴权流程及前后端联调逻辑。读者可直接复用签名生成逻辑、掌握access_token与jsapi_ticket的获取与缓存机制并通过sample.php快速验证扫码回调行为是微信公众号H5开发中扫码模块的典型实践参考。1. 这不是“微信扫一扫”SDK封装包而是一份被误标命名的 PHP 后端扫码业务逻辑参考实现你搜“php.zip_微信 扫一扫”点开一堆压缩包解压发现只有几个.php文件、一个config.php、几行curl调用和空荡荡的callback/目录——这不是微信官方 SDK也不是能直接跑起来的扫码页面。它本质是一套轻量级、无框架依赖的 PHP 后端服务骨架专为「生成带参数的微信扫码跳转链接」「接收扫码后的回调通知」这两个核心动作设计。它不处理前端摄像头调用、不集成 JS-SDK 签名、不对接微信支付但把code换access_token、ticket生成二维码、GET回调验签、POST数据解密AES这些真实线上项目里反复踩坑的脏活用不到 300 行原生 PHP 写透了。适合正在用 ThinkPHP/Laravel 做公众号菜单扫码活动、小程序推广码分发、或需要快速搭一个扫码统计后台的工程师——尤其当你发现官方文档里那句“请自行实现回调验签逻辑”让你头皮发麻时这份代码就是血泪经验的具象化。它不炫技不抽象所有函数名直白如gen_qr_code_by_scene_id()所有错误提示都带error_code40001这种微信原生码连curl_setopt的CURLOPT_SSL_VERIFYPEER都给你设成false并加注释“生产环境务必改回 true 并配好 CA 证书”。2. 从零跑通扫码闭环生成带参二维码 → 用户扫码 → 微信回调 → 服务端验签解密2.1 微信扫码业务链路再确认为什么必须自己写后端微信“扫一扫”功能在业务侧实际分两条线主动扫码用户打开微信 → 点“扫一扫” → 对准二维码触发的是微信服务器向你的服务器发起 HTTP POST 请求回调携带加密的用户信息被动展示你生成二维码 → 用户扫码 → 跳转到指定页面本质是生成一个weixin://dl/business/?txxx或https://mp.weixin.qq.com/qrcode?scenexxx链接由微信客户端解析跳转。很多人混淆二者以为“扫一扫”前端调 JS-SDK。错。JS-SDK 是用于网页内调用微信能力如分享、定位而扫码跳转的二维码生成和回调处理100% 是后端责任。这份php.zip正是聚焦后者——它不碰前端只做三件事① 把你的业务参数如user_id123sourceactivity_a编码进微信场景值scene_id② 调用微信 API 拿ticket生成可访问的二维码图片 URL③ 当微信 POST 数据过来时用你配置的AppSecret解密 AES 数据并验签。没有 Laravel 的 Service Provider没有 Composer autoload就index.phpcallback.phputils.php三个文件连require_once都只写两行。2.2 准备工作微信公众号后台配置与 PHP 环境硬性要求注意此方案仅适用于已认证的服务号测试号无法获取ticket生成永久二维码。订阅号不支持该接口。你需要提前在微信公众平台完成三件事开启【开发】→【基本配置】→ 获取AppID和AppSecret记牢后续全靠它在【公众号设置】→【功能设置】→ 【公众号二维码】中将你的服务器域名如api.yourdomain.com加入「JS接口安全域名」和「业务域名」否则回调会被微信拒绝在【开发】→【接口权限】→ 【公众号开发功能】中确保「生成带参数的二维码」权限已开通通常默认开启。PHP 环境要求明确版本 ≥ 7.2因使用json_encode($data, JSON_UNESCAPED_UNICODE)处理中文必须启用cURL扩展微信 API 全部走 HTTPS 请求必须启用openssl扩展回调数据 AES-256-CBC 解密必需file_get_contents需开启部分旧版utils.php用它读取本地 token 缓存建议改用 cURL。验证命令SSH 登录后执行php -v php -m | grep -E (curl|openssl) php -r echo extension_loaded(curl) extension_loaded(openssl) ? OK : MISSING;若输出MISSING小皮面板用户在「PHP 设置」→「扩展管理」勾选对应项宝塔用户在「软件商店」→「PHP 版本」→「设置」→「安装扩展」Linux 手动安装sudo apt install php-curl php-opensslUbuntu或sudo yum install php-curl php-opcacheCentOS。2.3 生成带参二维码gen_qr_code_by_scene_id()函数深度拆解核心逻辑在index.php中关键函数gen_qr_code_by_scene_id($scene_id, $expire_seconds 2592000)。它不是简单拼 URL而是严格遵循微信文档 创建二维码 的 POST 流程function gen_qr_code_by_scene_id($scene_id, $expire_seconds 2592000) { // 1. 先获取 access_token带本地缓存避免每秒请求 $token get_access_token(); if (!$token) return [error fail to get access_token]; // 2. 构造微信 API 请求体永久二维码用 scene_id临时码用 scene_str $post_data [ expire_seconds $expire_seconds, // 0 为永久二维码上限 10 万个 action_name $expire_seconds 0 ? QR_SCENE : QR_STR_SCENE, action_info [ scene $expire_seconds 0 ? [scene_id (int)$scene_id] : [scene_str (string)$scene_id] ] ]; // 3. 发起 POST 请求到微信服务器 $url https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token{$token}; $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data, JSON_UNESCAPED_UNICODE)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 生产环境务必改为 true 并设置 CURLOPT_CAINFO $result curl_exec($ch); curl_close($ch); // 4. 解析返回成功则含 ticket失败则含 errcode $resp json_decode($result, true); if (isset($resp[ticket])) { // 5. 用 ticket 拼出二维码图片 URL此 URL 可直接 img src... return [ qr_url https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket . urlencode($resp[ticket]), expire_seconds $expire_seconds, scene_id $scene_id ]; } else { return [error $resp[errmsg] ?? unknown error, errcode $resp[errcode] ?? 0]; } }参数说明与陷阱$scene_id必须是整数永久码或字符串临时码。微信对scene_id有严格限制永久码scene_id范围1~100000超出会返回errcode45058临时码scene_str长度 ≤ 32 字符且不能含特殊符号微信会截断或报错errcode40005$expire_seconds设为0生成永久二维码推荐活动页、客服入口设为6048007天适合短期裂变活动设为180030分钟适合登录验证类场景JSON_UNESCAPED_UNICODE关键不加此参数中文参数如scene_str用户_张三会变成\u5f20\u4e09微信回调时无法还原导致业务逻辑断裂CURLOPT_SSL_VERIFYPEER开发阶段设false避免证书问题但上线前必须改为true并通过CURLOPT_CAINFO指向系统 CA 证书路径如/etc/ssl/certs/ca-certificates.crt否则微信服务器可能拒绝连接。2.4 接收并验证微信回调handle_wechat_callback()的 AES 解密全流程当用户扫码后微信服务器会向你配置的callback.php地址发起 POST 请求数据是 AES-256-CBC 加密的 JSON 字符串且附带msg_signature、timestamp、nonce三个签名参数。callback.php的核心就是handle_wechat_callback()函数function handle_wechat_callback() { // 1. 获取原始 POST 数据不能用 $_POST微信发的是 raw body $raw_data file_get_contents(php://input); if (empty($raw_data)) { http_response_code(400); exit(no data); } // 2. 解析 URL 查询参数微信强制要求带 signature/timestamp/nonce $signature $_GET[msg_signature] ?? ; $timestamp $_GET[timestamp] ?? ; $nonce $_GET[nonce] ?? ; // 3. 验证签名用 AppSecret timestamp nonce raw_data 计算 SHA1 $tmpArr [$GLOBALS[APPSECRET], $timestamp, $nonce, $raw_data]; sort($tmpArr, SORT_STRING); $tmpStr implode($tmpArr); $calculated_signature sha1($tmpStr); if ($calculated_signature ! $signature) { http_response_code(403); exit(signature verify failed); } // 4. AES 解密微信用 AppSecret 作为 key用 timestamp 作为 iv注意iv 必须是 16 字节 $key base64_decode($GLOBALS[APPSECRET] . ); // 微信要求 AppSecret 补 后 base64_decode $iv substr($timestamp, 0, 16); // iv 取 timestamp 前 16 字符不足补 \0 if (strlen($iv) 16) { $iv str_pad($iv, 16, \0); } // 5. 执行解密PHP 7.1 推荐 openssl_decrypt兼容性优于 mcrypt $decrypted openssl_decrypt($raw_data, AES-256-CBC, $key, OPENSSL_RAW_DATA, $iv); if ($decrypted false) { error_log(AES decrypt failed: . openssl_error_string()); http_response_code(500); exit(decrypt failed); } // 6. 解析 JSON 并提取关键字段 $data json_decode($decrypted, true); if (json_last_error() ! JSON_ERROR_NONE) { http_response_code(400); exit(invalid json); } // 7. 返回 success 响应微信要求 XML 格式且必须是 xmlreturn_code![CDATA[SUCCESS]]/return_code/xml echo xmlreturn_code![CDATA[SUCCESS]]/return_code/xml; // 8. 此处写入你的业务逻辑如记录扫码用户 openid、更新活动参与状态等 $openid $data[FromUserName] ?? ; $scene_id $data[EventKey] ?? ; save_scan_record($openid, $scene_id, $data); }关键细节解释file_get_contents(php://input)绝对不能用$_POST微信 POST 的是 raw body$_POST为空sort($tmpArr, SORT_STRING)微信签名算法要求字典序排序SORT_STRING确保字符串比较而非数值$key base64_decode($GLOBALS[APPSECRET] . )这是微信文档明确要求的 key 生成方式APPSECRET末尾加再 base64_decode少一个字符都会解密失败$iv substr($timestamp, 0, 16)iv 必须严格 16 字节substr截取后若不足需str_pad补零否则openssl_decrypt返回falseecho xml.../xml微信回调要求必须返回此 XML且内容必须是SUCCESS否则会重复推送最多 3 次导致业务逻辑被执行多次。3. 避坑指南微信扫码回调里最常翻车的五个边界问题3.1 现象回调 URL 返回 500微信日志显示 “回调超时”原因callback.php中未正确设置max_execution_time或解密/数据库写入耗时过长微信要求响应时间 ≤ 5 秒。更隐蔽的是openssl_decrypt在 key/iv 错误时会静默失败$decrypted为false后续json_decode(false)触发 warning 导致 HTTP 响应头发送失败Nginx 记录为 500。解决在callback.php顶部加set_time_limit(3);限制脚本执行openssl_decrypt后立即检查if ($decrypted false)并error_log()所有数据库操作改用异步队列如 Redis List Worker 进程回调内只做日志记录和返回 SUCCESS。3.2 现象扫码后收不到回调或收到乱码 JSON原因$raw_data未做trim()处理微信 POST 数据末尾可能带换行符或 BOM 头导致sha1()签名计算偏差或APPSECRET配置文件中混入不可见空格如复制粘贴时带全角空格。解决$raw_data trim(file_get_contents(php://input));$GLOBALS[APPSECRET] trim($config[appsecret]);用hexdump -C callback.php | head -n 5检查文件是否含 BOMEF BB BF 开头。3.3 现象scene_id传入123abc报错errcode40005原因微信对临时二维码的scene_str参数校验极严只允许字母、数字、下划线、减号a-z A-Z 0-9 _ -且长度 ≤ 32。123abc合法但若实际传入123abc末尾空格或123abc含 符号即失败。解决生成前强制过滤preg_replace(/[^a-zA-Z0-9_-]/, , $scene_str)永久码必须用(int)$scene_id强制转换避免字符串123被微信当作scene_str处理。3.4 现象本地测试正常上线后回调验签总失败原因服务器时钟与微信服务器不同步误差 5 分钟导致timestamp参数失效或 Nginx/Apache 配置了fastcgi_buffering off但未关闭gzip导致php://input读取为空。解决ntpdate -s time.windows.com同步时间检查 Web 服务器配置Nginx 中添加gzip off;到location ~ \.php$ { ... }块内Apache 中确保mod_deflate未启用。3.5 现象二维码扫描后跳转到空白页或 404原因生成的qr_url未做urlencode()ticket中含/或字符被浏览器解析错误或微信后台未将你的域名加入「业务域名」白名单导致跳转被拦截。解决https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket . urlencode($resp[ticket])登录微信公众平台核对「公众号设置」→「功能设置」→ 「业务域名」是否包含https://yourdomain.com注意协议和子域名。4. 从 token 缓存到并发安全让扫码服务扛住 1000 QPS 的实战加固4.1access_token缓存策略为什么不能只靠文件锁微信access_token有效期 2 小时但每秒调用次数有限服务号 2000 次/天。若每次生成二维码都重新请求access_token不仅浪费配额更会在高并发时因网络延迟导致大量请求堆积。php.zip默认用file_put_contents(token_cache.txt, $token)做文件缓存这在单机环境可行但遇到以下情况立刻崩盘多进程如 Apache prefork同时读写同一文件file_get_contents读到半截数据NFS 共享存储下文件锁失效touch更新文件时间戳时多个进程判断time() - filemtime() 7200结果不一致。我一般会强制升级为 Redis 缓存兼容性最好PHP 原生支持// 替换 utils.php 中的 get_access_token() function get_access_token() { $redis new Redis(); $redis-connect(127.0.0.1, 6379); $key wechat:access_token: . $GLOBALS[APPID]; $token $redis-get($key); if ($token) return $token; // 缓存失效重新获取 $url https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid{$GLOBALS[APPID]}secret{$GLOBALS[APPSECRET]}; $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $result curl_exec($ch); curl_close($ch); $resp json_decode($result, true); if (isset($resp[access_token])) { // 设置过期时间微信返回 expires_in 是秒数Redis 用秒级 TTL $redis-setex($key, $resp[expires_in] - 300, $resp[access_token]); // 提前 5 分钟过期留重试窗口 return $resp[access_token]; } return false; }提示setex的 TTL 设为expires_in - 300预留 5 分钟缓冲期。当缓存即将过期时第一个请求会去刷新 token其他请求继续用旧 token微信允许短时间超期避免雪崩。4.2 并发写入扫码记录用 MySQL 行锁替代文件追加原始代码中save_scan_record()直接file_put_contents(log.txt, $data.PHP_EOL, FILE_APPEND)在 100 QPS 下会产生严重 IO 竞争日志错乱。更危险的是若业务逻辑涉及库存扣减如“扫码领优惠券”文件操作无法保证原子性。正确做法是用 InnoDB 行锁-- 建表语句关键主键 唯一索引防重复 CREATE TABLE wechat_scan_log ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 用户 openid, scene_id varchar(32) NOT NULL COMMENT 扫码场景值, scan_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, extra_data text COMMENT 微信回调原始数据, PRIMARY KEY (id), UNIQUE KEY uk_openid_scene (openid,scene_id) -- 防止同一用户重复扫码同一活动 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;PHP 写入逻辑function save_scan_record($openid, $scene_id, $data) { $pdo new PDO(mysql:hostlocalhost;dbnameyourdb;charsetutf8mb4, $user, $pass); $pdo-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); try { $stmt $pdo-prepare(INSERT INTO wechat_scan_log (openid, scene_id, extra_data) VALUES (?, ?, ?)); $stmt-execute([$openid, $scene_id, json_encode($data, JSON_UNESCAPED_UNICODE)]); } catch (PDOException $e) { if ($e-getCode() 23000) { // 1062 Duplicate entry唯一索引冲突 error_log(Duplicate scan: {$openid} - {$scene_id}); return; // 丢弃重复记录 } throw $e; } }为什么用UNIQUE KEY uk_openid_scene微信回调可能重试同一用户扫同一码会触发多次 POST。此索引确保数据库层面去重比 PHP 层判断SELECT COUNT更高效、更可靠。4.3 回调幂等性终极方案用 Redis SETNX 做分布式锁即使有数据库唯一索引极端情况下如网络抖动导致微信重复推送 数据库事务未提交仍可能漏判。最保险的是在回调入口加一层原子锁function handle_wechat_callback() { $raw_data trim(file_get_contents(php://input)); $signature $_GET[msg_signature] ?? ; $timestamp $_GET[timestamp] ?? ; $nonce $_GET[nonce] ?? ; // 1. 用 timestampnonceraw_data 的 hash 作为锁 key确保每次请求 key 唯一 $lock_key wechat:callback:lock: . md5($timestamp . $nonce . $raw_data); $redis new Redis(); $redis-connect(127.0.0.1, 6379); // 2. SETNX 命令仅当 key 不存在时设置返回 1 表示获得锁 if ($redis-setnx($lock_key, 1) 1) { $redis-expire($lock_key, 30); // 锁过期时间 30 秒防死锁 // 3. 执行验签、解密、业务逻辑 if (verify_signature($signature, $timestamp, $nonce, $raw_data)) { $decrypted aes_decrypt($raw_data, $timestamp); if ($decrypted) { save_scan_record_from_json($decrypted); } } // 4. 主动释放锁非必须expire 已兜底 $redis-del($lock_key); } else { // 5. 锁已被占用说明此请求已处理过直接返回 SUCCESS echo xmlreturn_code![CDATA[SUCCESS]]/return_code/xml; return; } }这个锁的价值它不依赖数据库事务不阻塞其他请求SETNX是 Redis 原子命令100% 保证同一请求只执行一次。即使save_scan_record因网络故障失败锁也会在 30 秒后自动释放下次重试可继续。5. 验证与压测用 curl 模拟微信回调用 ab 工具实测 QPS 上限5.1 手动构造微信回调请求绕过手机扫码10 秒复现全流程与其守着手机一遍遍扫码调试不如用curl直接模拟微信服务器行为。你需要三样东西一个真实的ticket从index.php生成二维码后拿到微信文档提供的 消息加解密工具 下载 ZIP运行aes_tool.exe你的AppSecret。步骤在aes_tool.exe中选择「解密」模式输入AppSecret、EncodingAESKey若没设置则用默认空、nonce随便填123456、timestamp当前 Unix 时间戳如1712345678、msg_signature工具会自动生成在「明文」框输入模拟数据{ ToUserName:gh_xxx, FromUserName:oxxx, CreateTime:1712345678, MsgType:event, Event:SCAN, EventKey:12345, Ticket:gQH47joAAAABMEj9YTX6m6VzNlPw7U8AAGbWAAVAyAQAAZ8DhBQAA }点击「加密」得到EncryptedData一长串 Base64 字符串用 curl 发送 POST 请求curl -X POST \ https://yourdomain.com/callback.php?msg_signaturexxxxxtimestamp1712345678nonce123456 \ -H Content-Type: application/json \ -d ENCRYPTED_BASE64_STRING_HERE \ -v观察响应是否为xmlreturn_code![CDATA[SUCCESS]]/return_code/xml并检查数据库是否新增记录。这比真机扫码快 10 倍且能精准控制每个参数。5.2 用 Apache Bench 实测并发能力找出你的瓶颈在哪ab是最轻量的压测工具无需安装复杂框架。假设你的index.php生成二维码接口地址为https://api.yourdomain.com/index.php?actiongen_qrscene_id1001# 并发 100 个请求总共 1000 次 ab -n 1000 -c 100 https://api.yourdomain.com/index.php?actiongen_qrscene_id1001 # 关键看这三项 # Requests per second: 125.32 [#/sec] QPS # Time per request: 798.090 [ms] 平均响应时间 # Failed requests: 0 失败数解读结果若 QPS 50瓶颈大概率在access_token获取未用 Redis 缓存或curlDNS 解析慢加curl_setopt($ch, CURLOPT_DNS_CACHE_TIMEOUT, 300)若响应时间 1000ms检查openssl_decrypt是否因 key/iv 错误进入死循环加microtime(true)日志定位若失败数 0通常是max_execution_time不足或内存溢出php.ini中memory_limit 128M。生产环境安全阈值场景安全 QPS应对措施普通活动页二维码生成≤ 200Redis 缓存 PHP OPcache 开启高频扫码登录每秒百人≤ 500Nginx upstream 负载均衡 MySQL 主从分离直播间弹幕扫码抢购≤ 2000改用 Swoole 协程 Redis Stream 消息队列5.3 最后一道防线在callback.php开头加日志熔断开关所有线上服务都要有“后悔药”。我在每个callback.php开头加一段熔断逻辑// 熔断开关当 1 分钟内错误率 30%自动返回 SUCCESS 不执行业务逻辑 $redis new Redis(); $redis-connect(127.0.0.1, 6379); $counter_key wechat:callback:error:counter; $success_key wechat:callback:success:counter; // 每次请求计数 $redis-incr($counter_key); $redis-expire($counter_key, 60); $redis-incr($success_key); $redis-expire($success_key, 60); // 计算错误率 $total $redis-get($counter_key) ?: 0; $success $redis-get($success_key) ?: 0; $error_rate $total 0 ? (1 - $success / $total) : 0; if ($error_rate 0.3) { error_log(CIRCUIT BREAKER TRIGGERED: error_rate{$error_rate}); echo xmlreturn_code![CDATA[SUCCESS]]/return_code/xml; exit; } // 正常流程...这个习惯救过我三次一次是 MySQL 主库宕机一次是 Redis 连接池打满一次是 OpenSSL 扩展异常。它不解决根本问题但能阻止错误雪崩给运维留出 5 分钟抢修时间。从那以后我每次上线新扫码活动都强制走一遍curl模拟 ab压测 熔断开关验证。不是怕代码错是怕人慌。希望帮到你。本文还有配套的精品资源点击获取
返回列表