微信 JS-SDK 分享功能是移動端網頁開發的常見需求。本文提供完整的「後端簽名 + 前端配置」落地方案,涵蓽 Ticket 緩存、簽名算法、前端 wx.config 注入、常見坑點排查,助你避開踩坑陷阱。
wx.config 初始化@Controller
@RequestMapping("/WeixinshareController/Api/Inteface")
public class WeixinshareController {
@Autowired private TicketRepositorySolr ticketRepositorySolr;
@RequestMapping("/getSignature")
public String getSignature(HttpServletRequest request, HttpServletResponse response)
throws IOException, ParseException {
String url = request.getParameter("url"); // 必須為完整頁面 URL(含查詢參數,去除 # 後片段)
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 1. 從資料庫/緩存獲取 Ticket
Ticket oldTicket = ticketRepositorySolr.getTicketById("固定_TICKET_ID");
if (oldTicket == null || isExpired(oldTicket, format)) {
// 首次或過期:重新向微信請求
return executeTicket(response, url, format);
} else {
// 有效:直接使用現有票據產生簽名
return buildSignatureResponse(response, oldTicket, url);
}
}
private boolean isExpired(Ticket ticket, SimpleDateFormat format) throws ParseException {
long diff = format.parse(format.format(new Date())).getTime()
- format.parse(ticket.getAcquiretime()).getTime();
return diff > 7_100_000; // 7200 秒 = 7_200_000 ms,留 100 秒緩衝
}
}
private String executeTicket(HttpServletResponse response, String url, SimpleDateFormat format)
throws IOException {
// 1. 產生隨機字串與時間戳
String noncestr = new GetRandomStr().getRandomString(15);
String timestamp = String.valueOf(System.currentTimeMillis());
// 2. 獲取 access_token
String tokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=YOUR_APPID&secret=YOUR_SECRET";
String tokenJson = WeixinUtil.httpRequest(tokenUrl, "GET", null);
String accessToken = new Gson().fromJson(tokenJson, ShareAccessToken.class).getAccess_token();
// 3. 獲取 jsapi_ticket
String ticketUrl = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + accessToken + "&type=jsapi";
String ticketJson = WeixinUtil.httpRequest(ticketUrl, "GET", null);
Ticket ticket = new Gson().fromJson(ticketJson, Ticket.class);
String jsapiTicket = ticket.getTicket();
// 4. 持久化 Ticket(含 noncestr、timestamp、acquiretime)
ticket.setTid("固定_TICKET_ID");
ticket.setNoncestr(noncestr);
ticket.setTimestamp(timestamp);
ticket.setAcquiretime(format.format(new Date()));
ticketRepositorySolr.addTicketToSolr(ticket);
// 5. 產生簽名並回應
return buildSignatureResponse(response, ticket, url);
}
private String buildSignatureResponse(HttpServletResponse response, Ticket ticket, String url)
throws IOException {
String signature = signature(ticket.getTicket(), ticket.getTimestamp(), ticket.getNoncestr(), url);
SignatureBean bean = new SignatureBean();
bean.setNoncestr(ticket.getNoncestr());
bean.setTimestamp(ticket.getTimestamp());
bean.setSignature(signature);
bean.setUrl(url);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(new Gson().toJson(bean));
return null;
}
private String signature(String jsapiTicket, String timestamp, String noncestr, String url) {
// 1. 組裝參數
String[] params = {
"jsapi_ticket=" + jsapiTicket,
"timestamp=" + timestamp,
"noncestr=" + noncestr,
"url=" + url
};
// 2. 字典序排序
Arrays.sort(params);
// 3. 拼接字串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < params.length; i++) {
sb.append(params[i]);
if (i != params.length - 1) sb.append("&");
}
// 4. SHA1 加密並轉十六進位
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(sb.toString().getBytes("UTF-8"));
return bytesToHex(digest);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-1 算法不可用", e);
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public class WeixinUtil {
public static String httpRequest(String url, String method, String output) {
try {
TrustManager[] tm = { new MyX509TrustManager() }; // 信任所有憑證(生產環境請妥善驗證)
SSLContext ssl = SSLContext.getInstance("SSL", "SunJSSE");
ssl.init(null, tm, new SecureRandom());
HttpsURLConnection conn = (HttpsURLConnection) new URL(url).openConnection();
conn.setSSLSocketFactory(ssl.getSocketFactory());
conn.setRequestMethod(method);
conn.setDoOutput(true);
conn.setDoInput(true);
if ("POST".equalsIgnoreCase(method) && output != null) {
try (OutputStream os = conn.getOutputStream()) {
os.write(output.getBytes("UTF-8"));
}
}
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) sb.append(line);
return sb.toString();
}
} catch (Exception e) {
throw new RuntimeException("微信 API 請求失敗", e);
}
}
}
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script>
var pageUrl = location.href.split('#')[0]; // 關鍵:去除 # 片段
$.get("WeixinshareController/Api/Inteface/getSignature", {url: pageUrl}, function(data) {
var config = JSON.parse(data);
wx.config({
debug: false,
appId: 'YOUR_APPID',
timestamp: config.timestamp,
nonceStr: config.noncestr,
signature: config.signature,
jsApiList: [
'onMenuShareTimeline', // 分享到朋友圈
'onMenuShareAppMessage', // 分享給朋友
'onMenuShareQQ',
'onMenuShareQZone'
]
});
});
wx.ready(function() {
var shareData = {
title: '明日醫療資訊',
desc: '精選醫療健康資訊',
link: pageUrl,
imgUrl: 'https://example.com/share-logo.png'
};
wx.onMenuShareTimeline(shareData);
wx.onMenuShareAppMessage(shareData);
wx.onMenuShareQQ(shareData);
wx.onMenuShareQZone(shareData);
});
wx.error(function(res) {
console.error('微信配置失敗:', res); // 開發期務必開啟 debug: true 排查
});
</script>
| 現象 | 可能原因 | 解決方案 |
|---|---|---|
| config:invalid signature | 簽名參數不一致 | 1. 確認 noncestr/timestamp 與 wx.config 完全一致 2. URL 必須為完整頁面 URL(含查詢參數,去除 #) 3. 簽名用的 jsapi_ticket 對應的 appid 必須與 wx.config 一致 |
| config:invalid url domain | 域名未綁定 | 微信公眾平台 → 開發配置 → JS介面安全域名 綁定正確域名 |
| config:ok 但分享不生效 | debug 為 true 干擾 | 生產環境設定 debug: false |
| 簽名驗證工具通過但實際失敗 | 前後端 noncestr/timestamp 不同步 | 後端簽名時使用的 noncestr/timestamp 必須原樣返回給前端,前端直接透傳給 wx.config |
location.href.split('#')[0],後端簽名時直接使用,不可重新組裝