ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信分享JSSDK签名验证PHP实现:从原理到完整代码

微信分享JSSDK签名验证PHP实现:从原理到完整代码 简介微信网页开发中后端签名验证是不少开发者绕不开的环节。这套代码把常见签名流程封装为一个PHP文件面向需要快速接入微信自定义分享能力的初、中级开发者适用于企业公众号、服务号及各类移动网页活动页下载后仅需替换公众号的应用标识与密钥即可直接使用。压缩包内共1个PHP文件大小约2KB轻量无依赖适合放入各类主流框架或原生项目。该文件已实现获取访问令牌、拉取临时票据、生成签名参数等核心逻辑并输出结构清晰、前端可直接用于初始化微信配置的数据能有效节省手工计算与联调时间。目前已有一千七百三十七人学习下载尤其适合想在已有网页中快速补充分享功能、又不想从头研究官方文档的开发同学。1. 微信分享 PHP 后端签名验证为什么容易卡在最后一步微信分享的 JSSDK 接入里前端拿到wx.config需要的signature是一个由后端计算出的 SHA1 散列值而这个计算过程本身不复杂把jsapi_ticket、noncestr、timestamp和当前页面 URL 拼成字符串做一次 SHA1 加密即可。真正让大多数 PHP 开发者卡住的地方往往不在算法而在三个隐藏条件jsapi_ticket必须通过 access_token 换取、timestamp必须与服务器当前时间对齐、签名用的 URL 必须与前端实际传递的 URL 保持一致。这套流程如果每次请求都现算现取很容易触发微信的接口频率限制也会出现签名验证失败这类让前端摸不着头脑的报错。这篇文章要解决的就是这个问题把完整的签名验证逻辑封装成一个 PHP 类附带 access_token 和 jsapi_ticket 的缓存机制配置好公众号 AppID 和 AppSecret 后直接调用即可。内容覆盖从原理到部署、再到常见报错排查的完整链路适合被微信分享折腾过、想省掉重复踩坑时间的后端开发者也适合需要快速给前端提供签名接口的独立开发者。2. 微信分享签名算法拆解与 PHP 实现2.1 签名串的生成规则微信 JSSDK 的签名验证本质上是服务端向微信服务器证明这个页面确实由我授权的公众号提供。签名串由四个参数按下述固定顺序拼接jsapi_ticketXXXXnoncestrYYYYtimestampZZZurl当前页面URL拼接顺序不能颠倒参数名不能缩写URL 必须完整。随后对这个字符串做 SHA1 散列得到 40 位十六进制字符串就是signature。前端拿到signature、timestamp、noncestr后调用wx.config完成初始化。有个容易忽略的细节URL 要去掉#号之后的部分。微信官方文档的表述是当前页面的完整 URL但实际测试中如果前端传了带 hash 的地址签名极易失败。常见做法是由后端接收前端window.location.href.split(#)[0]的结果而不是后端自己去猜页面地址。2.2 access_token 与 jsapi_ticket 的获取流程在计算签名之前必须先拿到jsapi_ticket它的获取链条是AppID AppSecret - access_token - jsapi_ticket - signatureaccess_token的接口是https://api.weixin.qq.com/cgi-bin/token通过 GET 请求提交grant_typeclient_credential、appid、secret三个参数返回 JSON 中包含access_token和expires_in有效期通常为 7200 秒。jsapi_ticket则需要用access_token去请求https://api.weixin.qq.com/cgi-bin/ticket/getticket?typejsapi同样返回ticket和expires_in。这两个凭证都必须缓存。如果不缓存每次计算签名都去请求微信接口很快会触发每日调用上限微信会返回errcode: 45009之类的限流错误。常见做法是存入文件或 Redis设置过期时间为 7000 秒留出 200 秒余量防止在临界点拿到过期凭证。2.3 PHP 签名类的完整代码下面是一个可直接使用的 PHP 类把获取、缓存、签名三个步骤封装到一起。这个类不依赖任何框架原生 PHP 环境即可运行。?php class WxShareSign { private $appId; private $appSecret; private $cacheDir; public function __construct($appId, $appSecret, $cacheDir __DIR__ . /cache) { $this-appId $appId; $this-appSecret $appSecret; $this-cacheDir $cacheDir; if (!is_dir($cacheDir)) { mkdir($cacheDir, 0755, true); } } /** * 对外暴露的签名方法 * param string $url 当前页面完整URL不要带#号 * return array */ public function getSignPackage($url) { $ticket $this-getJsApiTicket(); $timestamp time(); $nonceStr $this-createNonceStr(); // 按微信文档固定顺序拼接 $string jsapi_ticket{$ticket}noncestr{$nonceStr}timestamp{$timestamp}url{$url}; $signature sha1($string); return [ appId $this-appId, timestamp $timestamp, nonceStr $nonceStr, signature $signature, url $url, ]; } private function createNonceStr($length 16) { $chars abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789; $str ; for ($i 0; $i $length; $i) { $str . $chars[mt_rand(0, strlen($chars) - 1)]; } return $str; } private function getJsApiTicket() { $cacheFile $this-cacheDir . /jsapi_ticket.json; if (file_exists($cacheFile)) { $data json_decode(file_get_contents($cacheFile), true); if ($data $data[expire_time] time()) { return $data[ticket]; } } $accessToken $this-getAccessToken(); $url https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token{$accessToken}typejsapi; $result json_decode(file_get_contents($url), true); if (isset($result[ticket])) { file_put_contents($cacheFile, json_encode([ ticket $result[ticket], expire_time time() 7000, ])); return $result[ticket]; } throw new Exception(获取jsapi_ticket失败: . json_encode($result)); } private function getAccessToken() { $cacheFile $this-cacheDir . /access_token.json; if (file_exists($cacheFile)) { $data json_decode(file_get_contents($cacheFile), true); if ($data $data[expire_time] time()) { return $data[access_token]; } } $url https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid{$this-appId}secret{$this-appSecret}; $result json_decode(file_get_contents($url), true); if (isset($result[access_token])) { file_put_contents($cacheFile, json_encode([ access_token $result[access_token], expire_time time() 7000, ])); return $result[access_token]; } throw new Exception(获取access_token失败: . json_encode($result)); } }这段代码的核心思路是让调用方只关心getSignPackage一个方法。createNonceStr生成随机字符串使用mt_rand而不是rand在 PHP 7 之后两者性能差异不大但mt_rand的随机分布更好能尽量避免 nonceStr 重复。getJsApiTicket和getAccessToken都带文件缓存缓存文件名区分开避免两个凭证互相覆盖。2.4 调用方式与返回结构在任意 PHP 入口文件中引入这个类即可生成签名。以下是基于原生 PHP 的示例也适用于 ThinkPHP、Laravel 等框架的路由回调中?php require_once WxShareSign.php; $appId 你的AppID; $appSecret 你的AppSecret; $url isset($_GET[url]) ? $_GET[url] : ; if (empty($url)) { http_response_code(400); echo json_encode([errcode 400, errmsg url参数不能为空]); exit; } // 去掉URL中的#号部分这是签名失败的常见原因 $url explode(#, $url)[0]; $sign new WxShareSign($appId, $appSecret); $package $sign-getSignPackage($url); header(Content-Type: application/json); echo json_encode($package);这段接口代码让前端通过GET请求带上url参数就能拿到签名。explode(#, $url)[0]的处理非常关键因为很多前端习惯直接把window.location.href传过来其中可能包含#后面的路由信息导致后端签名用的 URL 与微信后台拿到的 URL 不一致。返回结构中的appId是给wx.config的appId字段用的nonceStr对应nonceStrtimestamp对应timestampsignature对应signature。3. 下载即用的目录结构与接入步骤3.1 最小可运行的项目文件清单下载即用的意思是拿到代码后只需要改配置、传到服务器、配好公众号域名就能跑通。一个最小的微信分享签名项目只需要三个文件文件作用WxShareSign.php签名类负责获取凭证和计算签名get_sign.phpHTTP 接口文件接收 URL 参数并返回 JSONcache/缓存目录存放 access_token 和 jsapi_ticket如果你用的是 Nginx PHP-FPM 的常见 LNMP 环境这三个文件放到站点根目录下的wxshare/子目录即可。cache/目录需要写入权限PHP-FPM 运行用户一般是www-data或者nginx确保这个用户对cache/有写权限否则缓存写不进去每次请求都会直接请求微信接口浪费配额不说还可能因为连续请求触发限流。3.2 在 Nginx 下配置路由与访问如果是 Apache直接访问文件路径就能工作不需额外配置。如果是 Nginx建议加一条 location 规则让接口路径看起来更干净同时避免 PHP 文件被直接下载这类安全隐患。location /wxshare/get_sign.php { fastcgi_pass unix:/run/php/php8.1-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }这个配置把/wxshare/get_sign.php单独匹配只让这个文件走 PHP-FPM。如果你的站点里还有其他 PHP 文件也可以不加这个规则让 Nginx 默认的 PHP location 处理。加上这条规则的额外好处是可以针对这个接口单独设置access_log off;避免签名请求频繁刷新日志文件。配置完成后重启 Nginx 和 PHP-FPMsudo nginx -t sudo systemctl reload nginx sudo systemctl reload php8.1-fpmnginx -t用来检查配置语法有报错会直接显示行号。reload是平滑重载不会中断当前连接适合线上环境。3.3 前端 wx.config 的对接方式后端签名接口就绪后前端需要先通过wx.ready和wx.error判断签名是否生效再做分享动作。下面是一段标准的前端调用代码const apiUrl /wxshare/get_sign.php?url encodeURIComponent(location.href.split(#)[0]); fetch(apiUrl) .then(response response.json()) .then(data { wx.config({ debug: false, appId: data.appId, timestamp: data.timestamp, nonceStr: data.nonceStr, signature: data.signature, jsApiList: [updateAppMessageShareData, updateTimelineShareData] }); wx.ready(function () { wx.updateAppMessageShareData({ title: 分享标题, desc: 分享描述, link: location.href.split(#)[0], imgUrl: 分享图标的绝对地址, success: function () { console.log(分享配置成功); } }); }); wx.error(function (res) { console.error(wx.config 失败:, res.errMsg); }); });location.href.split(#)[0]在前端再执行一次是为了确保传给后端签名的 URL 与实际注入分享链接的 URL 完全一致。imgUrl必须是微信能直接访问的绝对地址不能写相对路径否则分享卡片不显示缩略图。wx.error回调里的errMsg非常关键它通常会直接告诉你失败原因比如config:invalid signature或者config:invalid url domain前者是签名计算错误后者是公众号后台的 JS 接口安全域名没配好。3.4 验证签名是否有效的三种方法接口写好后先在浏览器里直接访问一次签名接口确认返回内容正常。接着做三种验证第一种是看返回的signature是不是 40 位十六进制字符串长度不对说明 SHA1 结果被截断或拼串里有非 ASCII 字符。第二种是手动复制返回的四个参数到微信官方文档的签名校验工具里核对。第三种是在前端打开debug: true微信会弹出config:ok提示说明签名验证通过。4. 签名验证失败的常见报错与参数排查4.1 invalid signature 的四个排查方向invalid signature是最常见的报错微信前端给出的信息只有这四个词真正问题的定位需要后端配合。常见做法是让前端在wx.error回调里把res.errMsg完整打到控制台然后后端把签名时使用的 URL、时间戳打印到日志里两相对照。第一个方向是 URL 不一致。前端传给后端的 URL 与微信后台登记的 JS 接口安全域名不匹配常见于本地开发调试时前端用的是localhost而后端签名接口部署在测试服务器上。解决方案是让前端在调用签名接口前把location.href发给后端后端原样使用不自己做拼接。第二个方向是timestamp过期。微信要求timestamp与服务器当前时间相差不超过一定范围如果服务器时间不准或者签名生成后隔了很久才调用wx.config就会出现config:invalid signature或x-timestamp 已过期这类提示。排查方法是登录服务器执行date -s校准时间建议直接开启 NTP 自动同步。第三个方向是jsapi_ticket与access_token缓存混用。如果多个公众号共用一份缓存或者缓存文件权限导致读取到旧内容就会用错误的 ticket 算签名。排查时把缓存文件删掉重新生成对比前后两次的signature是否发生变化。第四个方向是 URL 中的特殊字符。如果页面 URL 包含中文参数或空格拼接签名串之前必须保持原样不能做 urlencode。微信文档虽然没明确说但实际测试中 URL 编码后的字符串与未编码字符串计算出的签名完全不同。4.2 config:invalid url domain 的后台配置这个报错和签名算法无关纯粹是公众号后台的 JS 接口安全域名没有配置或配置错误。登录微信公众平台进入公众号设置-功能设置找到JS 接口安全域名填入签名接口所在域名的根域名不需要加http://或https://也不需要写具体路径。需要注意三个细节域名必须通过 ICP 备案文件名/MP_verify_xxxxxx.txt需要放到域名根目录下的指定位置微信后台会提供这个文件名修改域名后大约一分钟生效但建议等五分钟再试避免缓存还没刷新。如果前端页面跑在 IP 地址上这个方案就不适用微信只认域名。4.3 x-timestamp 已过期不是前端的问题不少开发者看到x-timestamp 已过期第一反应是前端本地时间不准但实际上这个时间戳是后端生成的过期原因几乎都在后端。常见情形是后端代码里用了某些框架的静态缓存把整个签名结果缓存了几分钟导致前端的timestamp与微信服务器当前时间差过大。解决方案是在签名类中不要缓存完整签名包只缓存jsapi_ticket和access_token。每个请求都重新计算timestamp和nonceStr这样签名包天然是新鲜的。如果业务上确实需要缓存签名结果缓存时间不要超过 30 秒并且要确保timestamp也是从缓存里取出的同一个值不能用新的timestamp配旧的signature。4.4 PHP 环境下 file_get_contents 被禁用的替代方案部分虚拟主机或安全加固过的 PHP 环境会禁用file_get_contents拉取远程 URL表现是签名接口直接报 500错误日志里出现Call to undefined function或http:// wrapper is disabled提示。遇到这种情况改用 cURL 扩展来请求微信接口这是虚拟主机上最通用的替代方案。private function httpGet($url) { $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $data curl_exec($ch); curl_close($ch); return $data; }CURLOPT_SSL_VERIFYPEER和CURLOPT_SSL_VERIFYHOST设为false是因为部分服务器的 CA 证书路径未配置不关闭的话 cURL 会直接拒绝连接。CURLOPT_TIMEOUT设为 10 秒是防止微信接口响应慢时请求长时间挂起拖垮 PHP-FPM 进程。把类中所有的file_get_contents替换为这个方法就能在禁用了远程文件读取的环境里正常工作。5. 进阶用 Redis 缓存签名凭证并支持多公众号切换5.1 Redis 与文件缓存的取舍文件缓存在单机低并发场景下完全够用但有两个隐患一是file_get_contents和file_put_contents不是原子操作多个请求同时写入同一个缓存文件时可能写坏 JSON二是多台服务器负载均衡时每台机器各存各的缓存会造成其中一台的 ticket 先过期引发间歇性签名失败。部署 Redis 后SETEX命令本身就是原子性的且所有服务器共享同一份缓存能同时解决这两个问题。5.2 改造签名类支持 Redis在原有类基础上新增一个构造参数$driver可选file或redis两个get和set方法做对应判断即可。改动量控制在 30 行以内不需要动签名计算逻辑。private function cacheGet($key) { if ($this-driver redis) { $value $this-redis-get($key); return $value ? json_decode($value, true) : null; } $file $this-cacheDir . / . $key . .json; if (file_exists($file)) { return json_decode(file_get_contents($file), true); } return null; } private function cacheSet($key, $data, $ttl 7000) { if ($this-driver redis) { $this-redis-setex($key, $ttl, json_encode($data)); return; } file_put_contents($this-cacheDir . / . $key . .json, json_encode($data)); }setex的第二个参数是过期时间微信凭证的有效期是 7200 秒这里存 7000 秒是为了让 PHP 侧缓存先于微信侧失效避免在过期临界点拿到失效的 ticket。Redis 连接推荐用PhpRedis扩展而不是predis原因是一个是 C 语言写的扩展性能更好另一个是纯 PHP 实现在并发高时消耗更多 CPU。5.3 多公众号的动态切换方法如果你的业务涉及多个公众号比如不同代理商有各自的公众号就不能把appId和appSecret写死在配置里。常见做法是根据前端传入的渠道标识在内存中维护一个公众号配置表动态实例化签名类。$appConfigs [ channel_a [appId xxx, appSecret yyy], channel_b [appId zzz, appSecret www], ]; $channel $_GET[channel] ?? channel_a; if (!isset($appConfigs[$channel])) { http_response_code(400); echo json_encode([errcode 400, errmsg 未知渠道]); exit; } $config $appConfigs[$channel]; $sign new WxShareSign($config[appId], $config[appSecret], __DIR__ . /cache); $package $sign-getSignPackage($url);切换公众号时要特别注意缓存 key 的隔离。原来的缓存文件直接用jsapi_ticket作为文件名多公众号共用必然串号。改造方式是在getJsApiTicket方法里把缓存 key 改成$this-appId . _jsapi_ticketaccess_token同理。用 Redis 时直接通过setex写入不同的 key 即可修改起来非常方便。多公众号场景下jsapi_ticket和access_token的独立性是必须保证的否则一个公众号的签名会算在另一个公众号头上。前端接入时只需额外传入channel参数与url参数一起拼接在请求地址中返回的签名包结构不变。这套方案直接解决了多公众号推广落地页的签名复用问题也让整个项目从单机单号平滑升级到水平和垂直扩展都支持的形态。本文还有配套的精品资源点击获取
返回列表