API v3版微信支付(二)----請求簽名、證書和回調報文解密


請求簽名是微信用來驗證請求的合法性的,簽名是放在請求頭中的編碼串。

簽名生成

商戶可以按照下述步驟生成請求的簽名。

微信支付API v3 key要求商戶對請求進行簽名。微信支付會在收到請求后進行簽名的驗證。如果簽名驗證不通過,微信支付API v3將會拒絕處理請求,並返回401 Unauthorized

准備

商戶需要擁有一個微信支付商戶號,並通過超級管理員賬號登錄商戶平台,獲取商戶API證書。商戶API證書 的壓縮包中包含了簽名必需的私鑰和商戶證書。

構造簽名串

我們希望商戶的技術開發人員按照當前文檔約定的規則構造簽名串。微信支付會使用同樣的方式構造簽名串。如果商戶構造簽名串的方式錯誤,將導致簽名驗證不通過。下面先說明簽名串的具體格式。

簽名串一共有五行,每一行為一個參數。行尾以 \n(換行符,ASCII編碼值為0x0A)結束,包括最后一行。如果參數本身以\n結束,也需要附加一個\n

HTTP請求方法\n
URL\n
請求時間戳\n
請求隨機串\n
請求報文主體\n

我們通過在命令行中調用"獲取微信支付平台證書"接口,一步一步向開發者介紹如何進行請求簽名。按照接口文檔,獲取商戶平台證書的URL為 https://api.mch.weixin.qq.com/v3/certificates請求方法為GET,沒有查詢參數。

第一步,獲取HTTP請求的方法(GET,POSTPUT)等

GET

第二步,獲取請求的絕對URL,並去除域名部分得到參與簽名的URL。如果請求中有查詢參數,URL末尾應附加有'?'和對應的查詢字符串。

/v3/certificates

第三步,獲取發起請求時的系統當前時間戳,即格林威治時間1970年01月01日00時00分00秒(北京時間1970年01月01日08時00分00秒)起至現在的總秒數,作為請求時間戳。微信支付會拒絕處理很久之前發起的請求,請商戶保持自身系統的時間准確。

$ date +%s 1554208460

第四步,生成一個請求隨機串(我們推薦生成隨機數算法如下:調用隨機數函數生成,將得到的值轉換為字符串) 。這里,我們使用命令行直接生成一個。

$ hexdump -n 16 -e '4/4 "%08X" 1 "\n"' /dev/random
593BEC0C930BF1AFEB40B4A08C8FB242

第五步,獲取請求中的請求報文主體(request body)。

  • 請求方法為GET時,報文主體為空。
  • 當請求方法為POST或PUT時,請使用真實發送的JSON報文。
  • 圖片上傳API,請使用meta對應的JSON報文。

對於下載證書的接口來說,請求報文主體是一個空串。

第六步,按照前述規則,構造的請求簽名串為:

GET\n 
/v3/certificates\n
1554208460\n
593BEC0C930BF1AFEB40B4A08C8FB242\n
\n

計算簽名值

絕大多數編程語言提供的簽名函數支持對簽名數據進行簽名。強烈建議商戶調用該類函數,使用商戶私鑰對待簽名串進行SHA256 with RSA簽名,並對簽名結果進行Base64編碼得到簽名值。

下面我們使用命令行演示如何生成簽名。


$ echo -n -e \ "GET\n/v3/certificates\n1554208460\n593BEC0C930BF1AFEB40B4A08C8FB242\n\n" \ | openssl dgst -sha256 -sign apiclient_key.pem \ | openssl base64 -A uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw== 

設置HTTP頭

微信支付商戶API v3要求請求通過HTTP Authorization頭來傳遞簽名。 Authorization認證類型和簽名信息兩個部分組成。

下面我們使用命令行演示如何生成簽名。

Authorization: 認證類型 簽名信息

具體組成為:

1.認證類型,目前為WECHATPAY2-SHA256-RSA2048

2.簽名信息

    • 發起請求的商戶(包括直連商戶、服務商或渠道商)的商戶號 mchid
    • 商戶API證書serial_no,用於聲明所使用的證書
    • 請求隨機串nonce_str
    • 時間戳timestamp
    • 簽名值signature

注:以上五項簽名信息,無順序要求。

Authorization 頭的示例如下:(注意,示例因為排版可能存在換行,實際數據應在一行)

Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930B

最終我們可以組一個包含了簽名的HTTP請求了。

$ curl https://api.mch.weixin.qq.com/v3/certificates -H 'Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"'

下面看一下具體代碼實現:

生成簽名
 /**
     * 生成簽名
     *
     * @param method
     * @param url
     * @param body
     * @return
     * @throws UnsupportedEncodingException
     * @throws SignatureException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    public static String getSign(String method, HttpUrl url, String body) throws UnsupportedEncodingException, SignatureException, NoSuchAlgorithmException, InvalidKeyException {
        String schema = "WECHATPAY2-SHA256-RSA2048";
        String nonceStr = "c5ac7061fccab6bf3e254dcf98995b8c";
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(method, url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"));

        return "mchid=\"" + CommonParameters.mchId + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + CommonParameters.mchSerialNo + "\","
                + "signature=\"" + signature + "\"";
    }

    static String sign(byte[] message) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnsupportedEncodingException {
        PrivateKey merchantPrivateKey = PemUtil
                .loadPrivateKey(new ByteArrayInputStream(CommonParameters.privateKey.getBytes("utf-8")));
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(merchantPrivateKey);
        sign.update(message);

        return Base64.getEncoder().encodeToString(sign.sign());
    }

    static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }

        return method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }

證書和回調報文解密

為了保證安全性,微信支付在回調通知和平台證書下載接口中,對關鍵信息進行了AES-256-GCM加密。本章節詳細介紹了加密報文的格式,以及如何進行解密。


加密報文格式

AES-GCM是一種NIST標准的認證加密算法, 是一種能夠同時保證數據的保密性、 完整性和真實性的一種加密模式。它最廣泛的應用是在TLS中。

證書和回調報文使用的加密密鑰為是一種NIST標准的APIv3密鑰

對於加密的數據,我們使用了一個獨立的JSON對象來表示。為了方便閱讀,示例做了Pretty格式化,並加入了注釋。

{
"original_type": "transaction", // 加密前的對象類型 "algorithm":"AEAD_AES_256_GCM", // 加密算法 // Base64編碼后的密文 "ciphertext": "...", // 加密使用的隨機串初始化向量) "nonce": "...", // 附加數據包(可能為空) "associated_data": "" }
加密的隨機串,跟簽名時使用的隨機串沒有任何關系,是不一樣的。
解密代碼實現(resource是請求的返回值)
            AesUtil aesUtil = new AesUtil(CommonParameters.apiV3Key.getBytes("utf-8"));
            String string = aesUtil.decryptToString(resource.getAssociated_data().getBytes("utf-8"), resource.getNonce().getBytes("utf-8"), resource.getCiphertext());

獲取平台證書並解密

/**
     * 獲取平台證書
     */
    public static X509Certificate getCertificates() throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        CloseableHttpClient httpClient = CommonUtils.httpClient();
        //請求URL
        HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
        httpGet.setHeader("Accept", "application/json");
        //生成簽名
        httpGet.setHeader("Authorization ", SignUtils.getSign("GET", HttpUrl.parse("https://api.mch.weixin.qq.com/v3/certificates"), ""));
        httpGet.setHeader("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
        //完成簽名並執行請求
        CloseableHttpResponse response = httpClient.execute(httpGet);
        X509Certificate x509Certificate = null;
        try {
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) { //處理成功
//                System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
                CertificateVo certificateVo = JSONObject.parseObject(EntityUtils.toString(response.getEntity()), CertificateVo.class);
                for (Certificates certificates : certificateVo.getData()) {
                    if (format.parse(certificates.getEffective_time()).before(new Date()) && format.parse(certificates.getExpire_time()).after(new Date())) {
                        EncryptCertificate encrypt_certificate = certificates.getEncrypt_certificate();
                        //解密
                        AesUtil aesUtil = new AesUtil(CommonParameters.apiV3Key.getBytes("utf-8"));
                        String pulicKey = aesUtil.decryptToString(encrypt_certificate.getAssociated_data().getBytes("utf-8"), encrypt_certificate.getNonce().getBytes("utf-8"), encrypt_certificate.getCiphertext());
                        
               //獲取平台證書
final CertificateFactory cf = CertificateFactory.getInstance("X509"); ByteArrayInputStream inputStream = new ByteArrayInputStream(pulicKey.getBytes(StandardCharsets.UTF_8)); x509Certificate = (X509Certificate) cf.generateCertificate(inputStream); } } return x509Certificate; } else if (statusCode == 204) { //處理成功,無返回Body System.out.println("success"); return x509Certificate; } else { System.out.println("failed,resp code = " + statusCode + ",return body = " + EntityUtils.toString(response.getEntity())); return x509Certificate; } } catch (GeneralSecurityException | ParseException e) { e.printStackTrace(); return null; } finally { response.close(); CommonUtils.after(httpClient); } }
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM