微信 JSSDK 允許網頁在微信客戶端內調用原生能力(分享、拍照、定位、支付等)。自定義分享是最常見的需求:將默認的「網頁標題+鏈接」替換為業務指定的標題、描述、圖片、鏈接。本文基於 ThinkPHP 框架,提供從後端獲取簽名到前端調用的完整實現方案。
appid + appsecret 獲取 access_token(緩存 7200 秒)。access_token 獲取 jsapi_ticket(緩存 7200 秒)。jsapi_ticket=...&noncestr=...×tamp=...&url=...,SHA1 加密 得到 signature。appId、timestamp、nonceStr、signature 傳給前端模板。jweixin-1.6.0.js,調用 wx.config() 注入權限,wx.ready() 中調用 wx.onMenuShareTimeline()、wx.onMenuShareAppMessage() 等分享接口。<?php
namespace app\controller;
use think\Controller;
use think\facade\Cache;
use think\facade\Request;
class WechatShare extends Controller {
// 配置常量(建議放入 config/wechat.php)
private const APPID = 'YOUR_APPID';
private const APPSECRET = 'YOUR_APPSECRET';
/**
* 分享頁面入口
*/
public function index() {
// 1. 獲取簽名所需參數
$signData = $this->getSignPackage();
// 2. 賦值給模板
$this->assign([
'appId' => self::APPID,
'timestamp' => $signData['timestamp'],
'nonceStr' => $signData['nonceStr'],
'signature' => $signData['signature'],
// 業務分享內容
'shareTitle' => '商品標題:超值大促鉅惠來襲',
'shareDesc' => '限時折扣,錯過不再有,快來搶購!',
'shareLink' => Request::url(true), // 當前頁面完整 URL
'shareImgUrl' => 'https://cdn.example.com/share/cover.jpg',
]);
return $this->fetch('share/index');
}
/**
* 獲取 JSSDK 簽名包
* @return array ['timestamp', 'nonceStr', 'signature']
*/
private function getSignPackage(): array {
$jsapiTicket = $this->getJsapiTicket();
$url = Request::url(true); // 當前頁面完整 URL(不含 #hash 部分)
$timestamp = time();
$nonceStr = $this->randomStr(16);
// 簽名字符串:參數按鍵名 ASCII 碼排序
$string = "jsapi_ticket={$jsapiTicket}&noncestr={$nonceStr}×tamp={$timestamp}&url={$url}";
$signature = sha1($string);
return compact('timestamp', 'nonceStr', 'signature');
}
/**
* 獲取 jsapi_ticket(緩存 7000 秒)
*/
private function getJsapiTicket(): string {
$cacheKey = 'wechat_jsapi_ticket_' . self::APPID;
$ticket = Cache::get($cacheKey);
if ($ticket) return $ticket;
$accessToken = $this->getAccessToken();
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={$accessToken}&type=jsapi";
$res = $this->httpGet($url);
$data = json_decode($res, true);
if (isset($data['errcode']) && $data['errcode'] !== 0) {
throw new \RuntimeException("獲取 jsapi_ticket 失敗: {$data['errmsg']}");
}
$ticket = $data['ticket'];
Cache::set($cacheKey, $ticket, 7000); // 提前 200 秒過期
return $ticket;
}
/**
* 獲取 access_token(緩存 7000 秒)
*/
private function getAccessToken(): string {
$cacheKey = 'wechat_access_token_' . self::APPID;
$token = Cache::get($cacheKey);
if ($token) return $token;
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" . self::APPID . "&secret=" . self::APPSECRET;
$res = $this->httpGet($url);
$data = json_decode($res, true);
if (isset($data['errcode']) && $data['errcode'] !== 0) {
throw new \RuntimeException("獲取 access_token 失敗: {$data['errmsg']}");
}
$token = $data['access_token'];
Cache::set($cacheKey, $token, 7000);
return $token;
}
/**
* 簡易 HTTP GET
*/
private function httpGet(string $url): string {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false, // 生產建議開啟並配置 CA
CURLOPT_SSL_VERIFYHOST => false,
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
/**
* 生成隨機字符串
*/
private function randomStr(int $length = 16): string {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$str = '';
for ($i = 0; $i < $length; $i++) {
$str .= $chars[random_int(0, strlen($chars) - 1)];
}
return $str;
}
/**
* 判斷是否微信瀏覽器
*/
public static function isWechat(): bool {
return strpos($_SERVER['HTTP_USER_AGENT'] ?? '', 'MicroMessenger') !== false;
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>{$shareTitle}</title>
<!-- 必須引入 JSSDK,支持 HTTPS -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<div class="container">
<h1>{$shareTitle}</h1>
<p>{$shareDesc}</p>
<img src="{$shareImgUrl}" alt="分享圖片" style="max-width: 100%">
</div>
<script>
// 1. 注入權限驗證配置
wx.config({
debug: false, // 生產環境關閉,開啟後會 alert 返回信息
appId: '{$appId}',
timestamp: {$timestamp},
nonceStr: '{$nonceStr}',
signature: '{$signature}',
// 必須聲明需要使用的接口列表
jsApiList: [
'onMenuShareTimeline', // 分享到朋友圈
'onMenuShareAppMessage', // 分享給朋友
'onMenuShareQQ', // 分享到 QQ
'onMenuShareWeibo', // 分享到微博
'updateAppMessageShareData', // 1.4.0+ 新版接口
'updateTimelineShareData'
]
});
// 2. 配置驗證成功後回調
wx.ready(function() {
// 通用分享數據
const shareData = {
title: '{$shareTitle}',
desc: '{$shareDesc}',
link: '{$shareLink}',
imgUrl: '{$shareImgUrl}',
};
// 新版接口(推薦,兼容性更好)
if (wx.updateAppMessageShareData) {
wx.updateAppMessageShareData(shareData);
wx.updateTimelineShareData(shareData);
} else {
// 舊版接口(兼容舊版微信)
wx.onMenuShareAppMessage({
...shareData,
success: () => console.log('分享給朋友成功'),
cancel: () => console.log('取消分享給朋友'),
});
wx.onMenuShareTimeline({
...shareData,
success: () => console.log('分享到朋友圈成功'),
cancel: () => console.log('取消分享到朋友圈'),
});
wx.onMenuShareQQ({ ...shareData });
wx.onMenuShareWeibo({ ...shareData });
}
});
// 3. 配置驗證失敗回調
wx.error(function(res) {
console.error('JSSDK 配置失敗:', res);
// 可上報錯誤到後端
});
</script>
</body>
</html>
| 問題 | 原因 | 解決 |
|---|---|---|
| 簽名驗證失敗 (invalid signature) | URL 不一致:簽名用的 URL 必須與當前頁面完全一致(含協議、域名、路徑、查詢參數,不含 #hash) | 後端用 Request::url(true) 獲取完整 URL,前端不做任何跳轉/重寫 |
| 分享出去標題/圖片不生效 | 未在 jsApiList 聲明接口、舊版接口已棄用、圖片域名未配置在「業務域名」白名單 | 聲明所有用到的接口、使用新版 updateAppMessageShareData、配置業務域名 |
| access_token/jsapi_ticket 失效 | 緩存過期未刷新、並發請求導致重複獲取、Token 被其他服務器刷新覆蓋 | 分布式環境用 Redis 分布式鎖刷新、單機用文件鎖、提前 200 秒主動刷新 |
| Android/iOS 表現不一致 | 微信客戶端版本差異、WebView 內核差異 | 同時兼容新舊接口、實機測試多機型 |
| HTTPS 證書問題 | 開發環境無有效證書、自簽證書不被信任 | 開發用內網穿透(ngrok/frp)映射到 HTTPS 域名、或配置信任自簽證書 |
微信後台 → 公眾號設置 → 功能設置 → JS 接口安全域名(舊稱「業務域名」)。
imgUrl 域名必須在該白名單內,否則圖片不顯示。深入掌握:PHP 微信開發技巧匯總、ThinkPHP 6 完整開發手冊、微信 JSSDK 官方文檔 等專題。