首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

微信 JS-SDK 簽名生成完整流程與 Java 後端實現代碼

     編輯:Java 相關知識

微信 JS-SDK 簽名算法概覽

調用微信 JS 接口(如分享、支付、掃碼)需配置簽名驗證,核心參數三元組:

  • noncestr:隨機字符串(UUID)
  • timestamp:Unix 時間戳(秒級)
  • signature:SHA1 簽名 = sha1(jsapi_ticket=...&noncestr=...&timestamp=...&url=...)

流程:access_token → jsapi_ticket → 簽名,其中 access_token 有效期 2 小時,jsapi_ticket 有效期 7200 秒,均需緩存複用。

Maven 依賴

<!-- 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>

完整 Java 實現

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);
        }
    }
}

HttpClient 工具類(簡化版)

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 signatureURL 必須為當前頁面完整 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 實現分佈式緩存。
  • 封裝為 Spring Boot Starter,提供 WeChatSignatureService Bean 注入使用。

總結

微信 JS-SDK 簽名核心在於「參數排序拼接 + SHA1 加密」,難點在於 access_token/jsapi_ticket 的緩存管理與 URL 一致性保證。本文提供的工具類可直接集成,生產環境請按現代化建議升級依賴與緩存策略。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved