調用微信 JS 接口(如分享、支付、掃碼)需配置簽名驗證,核心參數三元組:
sha1(jsapi_ticket=...&noncestr=...×tamp=...&url=...)流程:access_token → jsapi_ticket → 簽名,其中 access_token 有效期 2 小時,jsapi_ticket 有效期 7200 秒,均需緩存複用。
<!-- HttpClient 4.5 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!-- JSON-lib (舊版,建議遷移至 Jackson/Gson) -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
public class WeChatSignatureUtil {
private static final String APP_ID = "wx5f24fa0db1819ea2";
private static final String APP_SECRET = "uQtWzF0bQtl2KRHX0amekjpq8L0aO96LSpSNfctOBLRbuYPO4DUBhMn0_v2jHS-9";
private static final String TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken";
private static final String TICKET_URL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket";
// 緩存 access_token 與 jsapi_ticket(生產環境用 Redis/Guava Cache)
private static String cachedAccessToken;
private static String cachedJsapiTicket;
private static long tokenExpireTime;
private static long ticketExpireTime;
public static Map<String, String> generateSignature(String url) throws Exception {
String accessToken = getAccessToken();
String jsapiTicket = getJsapiTicket(accessToken);
String noncestr = UUID.randomUUID().toString();
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
// 簽名字符串:鍵值對按 ASCII 碼排序,URL 不編碼
String signStr = "jsapi_ticket=" + jsapiTicket +
"&noncestr=" + noncestr +
"×tamp=" + timestamp +
"&url=" + url;
String signature = sha1(signStr);
Map<String, String> result = new HashMap<>();
result.put("appId", APP_ID);
result.put("timestamp", timestamp);
result.put("nonceStr", noncestr);
result.put("signature", signature);
return result;
}
private static String getAccessToken() throws Exception {
if (cachedAccessToken != null && System.currentTimeMillis() < tokenExpireTime) {
return cachedAccessToken;
}
Map<String, String> params = Map.of("corpid", APP_ID, "corpsecret", APP_SECRET);
String resp = HttpClientUtil.post(TOKEN_URL, params);
JSONObject json = JSONObject.fromObject(resp);
cachedAccessToken = json.getString("access_token");
tokenExpireTime = System.currentTimeMillis() + 7000 * 1000; // 提前 200 秒過期
return cachedAccessToken;
}
private static String getJsapiTicket(String accessToken) throws Exception {
if (cachedJsapiTicket != null && System.currentTimeMillis() < ticketExpireTime) {
return cachedJsapiTicket;
}
Map<String, String> params = Map.of("access_token", accessToken);
String resp = HttpClientUtil.post(TICKET_URL, params);
JSONObject json = JSONObject.fromObject(resp);
cachedJsapiTicket = json.getString("ticket");
ticketExpireTime = System.currentTimeMillis() + 7000 * 1000;
return cachedJsapiTicket;
}
// SHA1 十六進制小寫輸出
private static String sha1(String str) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(str.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-1 算法不可用", e);
}
}
}
public class HttpClientUtil {
private static final CloseableHttpClient httpClient = HttpClients.createDefault();
public static String post(String url, Map<String, String> params) throws IOException {
HttpPost post = new HttpPost(url);
List<NameValuePair> nvps = params.entrySet().stream()
.map(e -> new BasicNameValuePair(e.getKey(), e.getValue()))
.collect(Collectors.toList());
post.setEntity(new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8));
try (CloseableHttpResponse response = httpClient.execute(post)) {
HttpEntity entity = response.getEntity();
return entity != null ? EntityUtils.toString(entity, StandardCharsets.UTF_8) : "";
}
}
}
wx.config({
debug: false,
appId: '${signature.appId}',
timestamp: ${signature.timestamp},
nonceStr: '${signature.nonceStr}',
signature: '${signature.signature}',
jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', 'chooseWXPay']
});
| 問題 | 原因 | 解決 |
|---|---|---|
| invalid signature | URL 必須為當前頁面完整 URL(不含 #hash),且與後端簽名用的 URL 完全一致 | 前端傳遞 location.href.split('#')[0] 給後端 |
| access_token 過期頻繁獲取被封 | 未緩存,並發請求重複調用 | 雙重檢查鎖 + 緩存,提前 200 秒主動刷新 |
| SHA1 結果大小寫不一致 | 微信要求小寫十六進制 | String.format("%02x", b) 保證小寫 |
| 企業號/服務號/訂閱號接口不同 | 企業微信用 corpid/corpsecret,公眾號用 appid/secret | 確認接口域名:qyapi.weixin.qq.com vs api.weixin.qq.com |
json-lib → Jackson / Gson(性能更好、維護活躍)。HttpClient 4.x → HttpClient 5.x / OkHttp / WebClient (Spring WebFlux)。Caffeine / Redis 實現分佈式緩存。WeChatSignatureService Bean 注入使用。微信 JS-SDK 簽名核心在於「參數排序拼接 + SHA1 加密」,難點在於 access_token/jsapi_ticket 的緩存管理與 URL 一致性保證。本文提供的工具類可直接集成,生產環境請按現代化建議升級依賴與緩存策略。