微信支付生成签名和验签SDK源码分析

目录

一、签名分析

1.1 流程分析

1.构造签名串

2.计算签名值

3.设置请求头

二、源码级别分析

二、获取平台证书分析

三、验签分析

3.1 验签使用场景:

 3.2 验证流程:

1.获取微信平台证书列表

2.检查平台证书序列号                         

3.2 验签源码分析

1.分析

2.总结:


        在商户系统(自开发)向微信支付平台系统发送请求,和接收结果的过程中,都是有生成签名和校验签名的过程,只是这些操作都被 SDK 的 生成CloseableHttpClient 之前已经处理过了。

        证书密钥使用说明-接口规则 | 微信支付商户平台文档中心

一、签名分析

1.1 流程分析

签名的生成分为3步骤,在SDK中也有代码的体现。

签名生成-接口规则 | 微信支付商户平台文档中心

 1.构造签名串

        签名串一共有五行,每一行为一个参数。行尾以 \n(换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以\n结束,也需要附加一个\n。

                HTTP请求方法\n                         GET\n 
                      URL\n                                            /v3/certificates\n
                      请求时间戳\n                                1554208460\n 
                      请求随机串\n                             593BEC0C930BF1AFEB40B4A08C8FB242\n 
                      请求报文主体\n                            {ifn:xxxx} \n

2.计算签名值

        绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。

        通过商户私钥使用 SHA256 withRSA 算法加密 ,在进行 BASE64 处理。

3.设置请求头

        微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization认证类型签名信息两个部分组成。

        认证类型 :这是使用什么类型进行加密处理

Authorization: 认证类型 签名信息

具体组成为:

1.认证类型,目前为WECHATPAY2-SHA256-RSA2048
	
2.签名信息
	
	发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
	商户API证书序列号serial_no,用于声明所使用的证书
	请求随机串nonce_str
	时间戳timestamp
	签名值signature
	注:以上五项签名信息,无顺序要求。



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"

二、源码级别分析

        1.通过log日志的打印得出 在生成 ScheduledUpdateCertificatesVerifier 校验器的时候已经 做好了签名的生成工作。

  • 生成签名串
  • 计算签名值
    •      4a35c18970fb03e4a36ef66ece03a935.png  
  • 设置HTTP请求信息 TOKEN参数的封装

       

2.通过日志分析得出处理类是 WechatPay2Credentials,进行了签名的处理。

public class WechatPay2Credentials implements Credentials {

    protected static final Logger log = LoggerFactory.getLogger(WechatPay2Credentials.class);

	// 私用符号
    protected static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
	// 安全随机数生成器
    protected static final SecureRandom RANDOM = new SecureRandom();
	// 商户编号
    protected final String merchantId;
    // 签名生成器 
	protected final Signer signer;
	
    public WechatPay2Credentials(String merchantId, Signer signer) {
        this.merchantId = merchantId;
        this.signer = signer;
    }

    public String getMerchantId() {
        return merchantId;
    }

	// 获取当前时间戳 
    protected long generateTimestamp() {
        return System.currentTimeMillis() / 1000;
    }

	// 生成字符串
    protected String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }

	// 获取认证类型
    @Override
    public final String getSchema() {
        return "WECHATPAY2-SHA256-RSA2048";
    }

	/**
	*  生成签名串+计算签名值+设置HTTP请求头
	*
	**/
    @Override
    public final String getToken(HttpRequestWrapper request) throws IOException {
		
		// 生成随机字符串
        String nonceStr = generateNonceStr();
		
		// 生成当前时间戳
        long timestamp = generateTimestamp();
		
		// 构造签名串
        String message = buildMessage(nonceStr, timestamp, request);
		
        log.debug("authorization message=[{}]", message);
		// 使用 PrivateKeySigner 进行签名算法处理 ,通过 商户私钥 + SHA256withRSA 的摘要计算 对签名数据进行加密处理 
        Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));

        String token = "mchid=\"" + getMerchantId() + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + signature.certificateSerialNumber + "\","
                + "signature=\"" + signature.sign + "\"";
        log.debug("authorization token=[{}]", token);

        return token;
    }

	/**
	* 构造签名串方法 
	* @Param nonce 随机字符串
	* @Param timestamp 当前时间戳
	* @Param request 请求对象
	*
	**/
    protected String buildMessage(String nonce, long timestamp, HttpRequestWrapper request) throws IOException {
		// 1.获取当前的请求路径对象,
        URI uri = request.getURI();
		
		// 2.获取到当前请求uri 路径 
        String canonicalUrl = uri.getRawPath(); // " /v3/certificates "
		
        // 3.判断是否有请求参数如果有则进行拼接上
		if (uri.getQuery() != null) {
            canonicalUrl += "?" + uri.getRawQuery();
        }

		// 4.签名主体
        String body = "";

		// 判断是否微信上传文件请求 请求方法为GET时,报文主体为空。
		// 当请求方法为POST或PUT时,请使用真实发送的JSON报文。
		// 图片上传API,请使用meta对应的JSON报文。
        // PATCH,POST,PUT
        if (request.getOriginal() instanceof WechatPayUploadHttpPost) {
            body = ((WechatPayUploadHttpPost) request.getOriginal()).getMeta();
			// 
        } else if (request instanceof HttpEntityEnclosingRequest) {
            body = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity(), StandardCharsets.UTF_8);
        }
		// 5.构建签名串 GET请求 body 是空的
        return request.getRequestLine().getMethod() + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }

}

        5953d89e72cef81cb751d309b90bbd30.png

        e0c220c277c6303ac240190897e0691d.png

/**
 * @author xy-peng
 */
public class PrivateKeySigner implements Signer {

	// 商户证书序列号 
    protected final String certificateSerialNumber;
	// 商户私钥 
    protected final PrivateKey privateKey;

    public PrivateKeySigner(String serialNumber, PrivateKey privateKey) {
        this.certificateSerialNumber = serialNumber;
        this.privateKey = privateKey;
    }

	// 对签名串进行摘要处理加密处理 
    @Override
    public SignatureResult sign(byte[] message) {
        try {
			
			// 创建SHA256withRSA加密方式的签名生成器实例对焦 
            Signature sign = Signature.getInstance("SHA256withRSA");
			
			// 初始化签名生成器,将商户私钥配置
            sign.initSign(privateKey);
			
			// 设置要加密的数据 
            sign.update(message);
			
			// 签名生成方法,并将结果进行 Base64的编码处理,并他封装签名结果对象 
            return new SignatureResult(Base64.getEncoder().encodeToString(sign.sign()), certificateSerialNumber);

        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
        } catch (SignatureException e) {
            throw new RuntimeException("签名计算失败", e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException("无效的私钥", e);
        }
    }

}

4.在 SignatureExec 的 executeWithSignature 方法中设置HTTP请求信息,并执行请求方法,获取请求结果,并进行结果的验签操作。

签名生成流程总结

        

  • 1.在获取 ScheduledUpdateCertificatesVerifier(签名校验器) 的过程中 调用 了 WechatPay2Credentials的 getToken 方法生成签名,该方法执行了生成: 1.签名串 2.将签名串进行加密处理
  • 2.getToken中调用了buildMessage 来生成签名串数据。
  • 3.buildMessage 方法中生成
  • 4.在getToken方法中调用了PrivateKeySigner 的 sign(), 这个方法底层调用 java. security 包下 的 SignatureSpi 的相关签名处理方法。
  • 5.执行完签名的后,SignatureExecexecuteWithSignature 方法中执行,设置HTTP请求头和执行请求方法来,并解析请求参数验证结果信息。

二、获取平台证书分析

        核心的类是 ScheduledUpdateCertificatesVerifier 是在原有CertificatesVerifier基础上,增加定时更新证书功能(默认1小时)

    /**
     * 初始化平台证书管理器实例,在使用前需先调用该方法
     *
     * @param credentials 认证器
     * @param apiV3Key APIv3密钥
     * @param minutesInterval 定时更新间隔时间
     */
    public synchronized void init(Credentials credentials, byte[] apiV3Key, long minutesInterval) {
        if (credentials == null || apiV3Key.length == 0 || minutesInterval == 0) {
            throw new IllegalArgumentException("credentials或apiV3Key或minutesInterval为空");
        }
        if (this.credentials == null || this.apiV3Key.length == 0 || this.executor == null
                || this.certificates == null) {
            this.credentials = credentials;
            this.apiV3Key = apiV3Key;
            this.executor = new SafeSingleScheduleExecutor();// 创建线程池
            this.certificates = new ConcurrentHashMap<>(); // 证书存放的Map

            // 初始化证书
            initCertificates();

            // 启动定时更新证书任务
            Runnable runnable = () -> {
                try {
                    Thread.currentThread().setName(SCHEDULE_UPDATE_CERT_THREAD_NAME);
                    log.info("Begin update Certificate.Date:{}", Instant.now());
                    updateCertificates(); // 更新证书的方法
                    log.info("Finish update Certificate.Date:{}", Instant.now());
                } catch (Throwable t) {
                    log.error("Update Certificate failed", t);
                }
            };
            // 执行线程任务 
            executor.scheduleAtFixedRate(runnable, 0, minutesInterval, TimeUnit.MINUTES);
        }
    }


	// 更新证书的方法
   private void updateCertificates() {
        Verifier verifier = null;
        if (!certificates.isEmpty()) {
            verifier = new CertificatesVerifier(certificates);
        }
       // 调用下载证书的方法
        downloadAndUpdateCert(verifier);
    }
// 初始化证书方法
private void initCertificates() {
        downloadAndUpdateCert(null);
    }
// 线程安全的证书下载方法
private synchronized void downloadAndUpdateCert(Verifier verifier) {
    // 创建出httpClient对象
        try (CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
                .withCredentials(credentials)
                .withValidator(verifier == null ? (response) -> true
                        : new WechatPay2Validator(verifier))
                .build()) {
            // 发送GET请求设置请求通信息
            HttpGet httpGet = new HttpGet(CERT_DOWNLOAD_PATH);
            httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString());
            // 发送请求
            try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
                // 获取结果code 
                int statusCode = response.getStatusLine().getStatusCode();
                // 获取结果数据 
                String body = EntityUtils.toString(response.getEntity());
                // 如果结果的 200 
                if (statusCode == SC_OK) {
                    // 将证书文件解码后存放到map中
                    Map<BigInteger, X509Certificate> newCertList = CertSerializeUtil.deserializeToCerts(apiV3Key, body);
                    if (newCertList.isEmpty()) {
                        log.warn("Cert list is empty");
                        return;
                    }
                    // 清除所有的证书
                    certificates.clear();
                    // 重新添加证书 
                    certificates.putAll(newCertList);
                } else {
                    log.error("Auto update cert failed, statusCode = {}, body = {}", statusCode, body);
                }
            }
        } catch (IOException | GeneralSecurityException e) {
            log.error("Download Certificate failed", e);
        }
    }

总结:

        证书每隔60分钟刷新证书,两个核心东西一个是定时器一个是存储身份信息对象(Credentials),证书更新是调用证书下载的方法,证书信息被放到响应体中被加密处理过,用的是对称加密的密钥,获取到证书数据后进行解密,然后存放到缓存Map中,每次重新获取证书都会清空当前map。

三、验签分析

3.1 验签使用场景:

        

需要验证签名的时间点分为两处:

  • 1.商户(我们系统)主要向微信支付系统发送,微信支付系统向我们响应结果时。

 6eb8f80b0ebe695b021690cb7ff09dc6.png

  • 2.微信系统回调商户系统,需要对数据验证操作,防止伪造数据

注意应答签名验证是可做可不做的,回调必须要验证签名

        如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。

 3.2 验证流程:

        签名验证-接口规则 | 微信支付商户平台文档中心

1.获取微信平台证书列表

  • 接口:https://api.mch.weixin.qq.com/v3/certificates
  • 在上方验签请求就是在获取微信平台证书。
  • 微信支付的公钥只能从从微信平台证书中获取
  • 微信支付的公钥只能从从微信平台证书中获取
  • 平台证书是一个列表,因为证书是有一个有效期的,如果证书过期,新的证书还没有生成,那么就会有一个证书的空档期,所有微信会把提前24小时的新证书加入到平台证书列表中,所以平台证书会存在两个一个是将要过期的,一个即将启用的。
  • 返回结果会提供微信支付平台证书的序列号,serial_no 进行证书区分。
  • 200: OK
    {
      "data": [
          {
              "serial_no": "5157F09EFDC096DE15EBE81A47057A7232F1B8E1",
              "effective_time ": "2018-06-08T10:34:56+08:00",
              "expire_time ": "2018-12-08T10:34:56+08:00",
              "encrypt_certificate": {
                  "algorithm": "AEAD_AES_256_GCM",
                  "nonce": "61f9c719728a",
                  "associated_data": "certificate",
                  "ciphertext": "sRvt… "
              }
          },
          {
              "serial_no": "50062CE505775F070CAB06E697F1BBD1AD4F4D87",
              "effective_time ": "2018-12-07T10:34:56+08:00",
              "expire_time ": "2020-12-07T10:34:56+08:00",
              "encrypt_certificate": {
                  "algorithm": "AEAD_AES_256_GCM",
                  "nonce": "35f9c719727b",
                  "associated_data": "certificate",
                  "ciphertext": "aBvt… "
              }
          }
      ]
    }

2.检查平台证书序列号 

        微信支付的平台证书序列号位于HTTP头Wechatpay-Serial。验证签名前,请商户先检查序列号是否跟商户当前所持有的 微信支付平台证书的序列号一致。如果不一致,请重新获取证书。否则,签名的私钥和证书不匹配,将无法成功验证签名。

3.构造签名串

  • 首先,商户先从应答中获取以下信息:
    • HTTP头Wechatpay-Timestamp 中的应答时间戳
    • HTTP头Wechatpay-Nonce 中的应答随机串。
    • 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。
    • 然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。

应答时间戳\n
应答随机串\n
应答报文主体\n

  • 如某个应答的HTTP报文为(省略了ciphertext的具体内容):
  • HTTP/1.1 200 OK
    Server: nginx
    Date: Tue, 02 Apr 2019 12:59:40 GMT
    Content-Type: application/json; charset=utf-8
    Content-Length: 2204
    Connection: keep-alive
    Keep-Alive: timeout=8
    Content-Language: zh-CN
    Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
    Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c // 应答随机串
    // 应答签名
    Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
    Wechatpay-Timestamp: 1554209980 // 应答时间戳
    Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
    Cache-Control: no-cache, must-revalidate
    // 应答报文主体
    {"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}

    • 则验签名串为
    • 1554209980
      c5ac7061fccab6bf3e254dcf98995b8c
      {"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}

                               

4.获取应答签名

  • 微信支付的应答签名通过HTTP头Wechatpay-Signature传递。(注意,示例因为排版可能存在换行,实际数据应在一行)
  • Wechatpay-Signature的字段值使用Base64进行解码,得到应答签名。

Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==

5.验证签名

    • 很多编程语言的签名验证函数支持对验签名串和签名 进行签名验证。强烈建议商户调用该类函数,使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证。
    • (1) 首先,从微信支付平台证书导出微信支付平台公钥
    • (2) 然后,把签名base64解码。
    • (3) 最后验证签名,验证签名的流程是:
      • 1.拿到刚刚生成签名,使用 SHA256 算法+ 微信公钥 进行摘要计算。
      • 2.将得到的结果和 应到签名进行比较判断是否一致,如果一致则为正常,否则失败。

3.2 验签源码分析

1.分析

        通过源码分析得出真正的验签的类是 WechatPay2Validator ,这个类中有一个方法 validate 进行执行验证操作。

package com.wechat.pay.contrib.apache.httpclient.auth;


import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.REQUEST_ID;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_NONCE;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SIGNATURE;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_TIMESTAMP;

import com.wechat.pay.contrib.apache.httpclient.Validator;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author xy-peng
 */
public class WechatPay2Validator implements Validator {

    protected static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
    /**
     * 应答超时时间,单位为分钟
     */
    protected static final long RESPONSE_EXPIRED_MINUTES = 5;
    // 验证器
    protected final Verifier verifier;
	
    public WechatPay2Validator(Verifier verifier) {
        this.verifier = verifier;
    }
	// 参数异常处理方法
    protected static IllegalArgumentException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }

    // 验证失败处理异常
    protected static IllegalArgumentException verifyFail(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("signature verify fail: " + message);
    }

    // 核心方法验证方法
    @Override
    public final boolean validate(CloseableHttpResponse response) throws IOException {
        try {
            // 验证响应的结果参数是
            validateParameters(response);
            // 生成签名
            String message = buildMessage(response);
            // 获取微信平台证书序列号
            String serial = response.getFirstHeader(WECHAT_PAY_SERIAL).getValue();
            // 获取当前应答签名
            String signature = response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue();

            // 验签比较方法,
            if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
                throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                        serial, message, signature, response.getFirstHeader(REQUEST_ID).getValue());
            }
        } catch (IllegalArgumentException e) {
            log.warn(e.getMessage());
            return false;
        }

        return true;
    }
	// 校验参数方法
    protected final void validateParameters(CloseableHttpResponse response) {
        // 获取第一个请求为 Request-ID 
        Header firstHeader = response.getFirstHeader(REQUEST_ID);
        // 如果请求头为null则验证失败 
        if (firstHeader == null) {
            throw parameterError("empty " + REQUEST_ID);
        }
        // 获取请求id 
        String requestId = firstHeader.getValue();

        // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
        // 组装一个 响应的参数名称集合 有 证书序列号、应答签名、时间戳、随机字符串
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};

        Header header = null;
        // 遍历所需要的请求的参数名取从应答对象中拿如果没有则报错
        for (String headerName : headers) {
            // 拿去所需要的应答对象数据
            header = response.getFirstHeader(headerName);
            if (header == null) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }
    	// 因为最后的值是时间戳,所以这里直接获取到时间戳了
        String timestampStr = header.getValue();
        
        try {
            // 将时间戳进行转换为时间
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 比较应答时间和当前时间超过默认5分钟, 拒绝过期应答
            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
            }
        } catch (DateTimeException | NumberFormatException e) {
            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
        }
    }

    // 验签名串
    protected final String buildMessage(CloseableHttpResponse response) throws IOException {
        String timestamp = response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue();
        String nonce = response.getFirstHeader(WECHAT_PAY_NONCE).getValue();
        String body = getResponseBody(response);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }
	// 获取报文主体结果
    protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();
        return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
    }

}
  • 真正验签会走到 CertificatesVerifier 的 verify 方法
  •  certificates.get(val) 是根据 微信平台证书序列号从map中获取证书信息
  • 获取到验证器会调用 verify 进行组装 参数 最终调用 Signature 的 verify 进行验证
    protected boolean verify(X509Certificate certificate, byte[] message, String signature) {
        try {
            // 创建签名器,并指定签名算法
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initVerify(certificate); // 设置签名器使用的证书公钥
            sign.update(message);// 要验证的数据
            // 验证传入的签名数据  也就是 update 和 verify 数据进行比较
            return sign.verify(Base64.getDecoder().decode(signature));

        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
        } catch (SignatureException e) {
            throw new RuntimeException("签名验证过程发生了错误", e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException("无效的证书", e);
        }
    }

 ​​​​​​​

 

  • 从图中可以看出 传来参数 var1 就是应答的签名,首先对应答对象进行了 rsa 公钥的解密得到 var3
  • var2 是对生成的签名串进行摘要计算
  • 最后将应答签名和生成的签名进行equal比较。
// sigBytes 应答的签名结果
@Override
    protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
        if (publicKey == null) {
            throw new SignatureException("Missing public key");
        }
        try {
            if (sigBytes.length != RSACore.getByteLength(publicKey)) {
                throw new SignatureException("Signature length not correct: got " +
                    sigBytes.length + " but was expecting " +
                    RSACore.getByteLength(publicKey));
            }
            // 获取生成的签名串的摘要结果
            byte[] digest = getDigestValue();
            // 将应答数据进行rsa使用微信公钥解密
            byte[] decrypted = RSACore.rsa(sigBytes, publicKey);
            // 将处理的值进行拆包处理
            byte[] unpadded = padding.unpad(decrypted);
            // 使用digestOID 进行解密处理 得出 摘要的数据
            byte[] decodedDigest = decodeSignature(digestOID, unpadded);
            // 将摘要的值和应答的值进行equal比较
            return MessageDigest.isEqual(digest, decodedDigest);
        } catch (javax.crypto.BadPaddingException e) {
            // occurs if the app has used the wrong RSA public key
            // or if sigBytes is invalid
            // return false rather than propagating the exception for
            // compatibility/ease of use
            return false;
        } catch (IOException e) {
            throw new SignatureException("Signature encoding error", e);
        } finally {
            resetDigest();
        }
    }

2.总结:

e1999672bb7e9be8fb9d5748193f9594.png

        正常的签名流程是 是对数据先进行摘要,在进行加密处理,然后在进行Base64的处理,验签也是反过来,最后一部就是验证签名

1.获取应答对象的请求头数据

2.将数据生成签名串

3.获取应答签名串

4.将生成的签名串进行摘要计算,生成摘要串。

5.使用rsa算法对应答签名进行解密得出摘要结果。

6.将生成的摘要串和解密的摘要串进行比较 。

        检验的话他会先将用公钥对解码后(base64)的数据进行解密获取到加密的数据,在后用 使用rsa算法+微信公钥进行解码得出 sha256 对之前获取到微信的签名串数据进行摘要运算,在将在将两个摘要数据进行比较是否相同从而验证是否合法性。因为摘要运算,相同的数据得出的结果都是相同的。

  • 22
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
微信支付 SDK 的基本流程如下: 1. 从微信支付服务器获取支付结果通知(XML 格式)。 2. 将 XML 格式的支付结果通知转换成 Map 对象。 3. 对 Map 对象中的参数按照 key 值进行字典序排序。 4. 将排序后的参数拼接成一个字符串,格式为 key1=value1&key2=value2&...&keyn=valuen。 5. 将拼接后的字符串与商户密钥进行拼接,得到一个新的字符串。 6. 对新的字符串进行 MD5 签名,得到一个签名值。 7. 将签名值与支付结果通知中的 sign 字段进行比较,如果一致则代表通过。 以下是一个 Java 实现的示例代码: ```java import java.util.*; import java.security.*; import javax.xml.bind.DatatypeConverter; public class WxPay { // 商户密钥 private static final String KEY = "your_key_here"; // 微信支付通知 public static boolean verify(Map<String, String> params) { // 将参数按照 key 值进行字典序排序 List<String> keys = new ArrayList<String>(params.keySet()); Collections.sort(keys); // 拼接参数字符串 String paramStr = ""; for (String key : keys) { String value = params.get(key); if (!value.isEmpty() && !key.equals("sign")) { paramStr += key + "=" + value + "&"; } } paramStr += "key=" + KEY; // 对参数字符串进行 MD5 签名 String sign = null; try { MessageDigest md5 = MessageDigest.getInstance("MD5"); byte[] bytes = md5.digest(paramStr.getBytes("UTF-8")); sign = DatatypeConverter.printHexBinary(bytes).toLowerCase(); } catch (Exception e) { return false; } // 比较签名值 return sign.equals(params.get("sign")); } } ``` 调用示例: ```java // 获取支付结果通知 Map<String, String> params = ...; // 支付通知 if (WxPay.verify(params)) { // 通过,处理支付结果 } else { // 失败,忽略该支付通知 } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值