当前位置:   article > 正文

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

微信支付authorization

目录

一、签名分析

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认证类型签名信息两个部分组成。

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

  1. Authorization: 认证类型 签名信息
  2. 具体组成为:
  3. 1.认证类型,目前为WECHATPAY2-SHA256-RSA2048
  4. 2.签名信息
  5. 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid
  6. 商户API证书序列号serial_no,用于声明所使用的证书
  7. 请求随机串nonce_str
  8. 时间戳timestamp
  9. 签名值signature
  10. 注:以上五项签名信息,无顺序要求。
  11. 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,进行了签名的处理。

  1. public class WechatPay2Credentials implements Credentials {
  2. protected static final Logger log = LoggerFactory.getLogger(WechatPay2Credentials.class);
  3. // 私用符号
  4. protected static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  5. // 安全随机数生成器
  6. protected static final SecureRandom RANDOM = new SecureRandom();
  7. // 商户编号
  8. protected final String merchantId;
  9. // 签名生成器
  10. protected final Signer signer;
  11. public WechatPay2Credentials(String merchantId, Signer signer) {
  12. this.merchantId = merchantId;
  13. this.signer = signer;
  14. }
  15. public String getMerchantId() {
  16. return merchantId;
  17. }
  18. // 获取当前时间戳
  19. protected long generateTimestamp() {
  20. return System.currentTimeMillis() / 1000;
  21. }
  22. // 生成字符串
  23. protected String generateNonceStr() {
  24. char[] nonceChars = new char[32];
  25. for (int index = 0; index < nonceChars.length; ++index) {
  26. nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
  27. }
  28. return new String(nonceChars);
  29. }
  30. // 获取认证类型
  31. @Override
  32. public final String getSchema() {
  33. return "WECHATPAY2-SHA256-RSA2048";
  34. }
  35. /**
  36. * 生成签名串+计算签名值+设置HTTP请求头
  37. *
  38. **/
  39. @Override
  40. public final String getToken(HttpRequestWrapper request) throws IOException {
  41. // 生成随机字符串
  42. String nonceStr = generateNonceStr();
  43. // 生成当前时间戳
  44. long timestamp = generateTimestamp();
  45. // 构造签名串
  46. String message = buildMessage(nonceStr, timestamp, request);
  47. log.debug("authorization message=[{}]", message);
  48. // 使用 PrivateKeySigner 进行签名算法处理 ,通过 商户私钥 + SHA256withRSA 的摘要计算 对签名数据进行加密处理
  49. Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));
  50. String token = "mchid=\"" + getMerchantId() + "\","
  51. + "nonce_str=\"" + nonceStr + "\","
  52. + "timestamp=\"" + timestamp + "\","
  53. + "serial_no=\"" + signature.certificateSerialNumber + "\","
  54. + "signature=\"" + signature.sign + "\"";
  55. log.debug("authorization token=[{}]", token);
  56. return token;
  57. }
  58. /**
  59. * 构造签名串方法
  60. * @Param nonce 随机字符串
  61. * @Param timestamp 当前时间戳
  62. * @Param request 请求对象
  63. *
  64. **/
  65. protected String buildMessage(String nonce, long timestamp, HttpRequestWrapper request) throws IOException {
  66. // 1.获取当前的请求路径对象,
  67. URI uri = request.getURI();
  68. // 2.获取到当前请求uri 路径
  69. String canonicalUrl = uri.getRawPath(); // " /v3/certificates "
  70. // 3.判断是否有请求参数如果有则进行拼接上
  71. if (uri.getQuery() != null) {
  72. canonicalUrl += "?" + uri.getRawQuery();
  73. }
  74. // 4.签名主体
  75. String body = "";
  76. // 判断是否微信上传文件请求 请求方法为GET时,报文主体为空。
  77. // 当请求方法为POST或PUT时,请使用真实发送的JSON报文。
  78. // 图片上传API,请使用meta对应的JSON报文。
  79. // PATCH,POST,PUT
  80. if (request.getOriginal() instanceof WechatPayUploadHttpPost) {
  81. body = ((WechatPayUploadHttpPost) request.getOriginal()).getMeta();
  82. //
  83. } else if (request instanceof HttpEntityEnclosingRequest) {
  84. body = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity(), StandardCharsets.UTF_8);
  85. }
  86. // 5.构建签名串 GET请求 body 是空的
  87. return request.getRequestLine().getMethod() + "\n"
  88. + canonicalUrl + "\n"
  89. + timestamp + "\n"
  90. + nonce + "\n"
  91. + body + "\n";
  92. }
  93. }

        5953d89e72cef81cb751d309b90bbd30.png

        e0c220c277c6303ac240190897e0691d.png

  1. /**
  2. * @author xy-peng
  3. */
  4. public class PrivateKeySigner implements Signer {
  5. // 商户证书序列号
  6. protected final String certificateSerialNumber;
  7. // 商户私钥
  8. protected final PrivateKey privateKey;
  9. public PrivateKeySigner(String serialNumber, PrivateKey privateKey) {
  10. this.certificateSerialNumber = serialNumber;
  11. this.privateKey = privateKey;
  12. }
  13. // 对签名串进行摘要处理加密处理
  14. @Override
  15. public SignatureResult sign(byte[] message) {
  16. try {
  17. // 创建SHA256withRSA加密方式的签名生成器实例对焦
  18. Signature sign = Signature.getInstance("SHA256withRSA");
  19. // 初始化签名生成器,将商户私钥配置
  20. sign.initSign(privateKey);
  21. // 设置要加密的数据
  22. sign.update(message);
  23. // 签名生成方法,并将结果进行 Base64的编码处理,并他封装签名结果对象
  24. return new SignatureResult(Base64.getEncoder().encodeToString(sign.sign()), certificateSerialNumber);
  25. } catch (NoSuchAlgorithmException e) {
  26. throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
  27. } catch (SignatureException e) {
  28. throw new RuntimeException("签名计算失败", e);
  29. } catch (InvalidKeyException e) {
  30. throw new RuntimeException("无效的私钥", e);
  31. }
  32. }
  33. }

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小时)

  1. /**
  2. * 初始化平台证书管理器实例,在使用前需先调用该方法
  3. *
  4. * @param credentials 认证器
  5. * @param apiV3Key APIv3密钥
  6. * @param minutesInterval 定时更新间隔时间
  7. */
  8. public synchronized void init(Credentials credentials, byte[] apiV3Key, long minutesInterval) {
  9. if (credentials == null || apiV3Key.length == 0 || minutesInterval == 0) {
  10. throw new IllegalArgumentException("credentials或apiV3Key或minutesInterval为空");
  11. }
  12. if (this.credentials == null || this.apiV3Key.length == 0 || this.executor == null
  13. || this.certificates == null) {
  14. this.credentials = credentials;
  15. this.apiV3Key = apiV3Key;
  16. this.executor = new SafeSingleScheduleExecutor();// 创建线程池
  17. this.certificates = new ConcurrentHashMap<>(); // 证书存放的Map
  18. // 初始化证书
  19. initCertificates();
  20. // 启动定时更新证书任务
  21. Runnable runnable = () -> {
  22. try {
  23. Thread.currentThread().setName(SCHEDULE_UPDATE_CERT_THREAD_NAME);
  24. log.info("Begin update Certificate.Date:{}", Instant.now());
  25. updateCertificates(); // 更新证书的方法
  26. log.info("Finish update Certificate.Date:{}", Instant.now());
  27. } catch (Throwable t) {
  28. log.error("Update Certificate failed", t);
  29. }
  30. };
  31. // 执行线程任务
  32. executor.scheduleAtFixedRate(runnable, 0, minutesInterval, TimeUnit.MINUTES);
  33. }
  34. }
  35. // 更新证书的方法
  36. private void updateCertificates() {
  37. Verifier verifier = null;
  38. if (!certificates.isEmpty()) {
  39. verifier = new CertificatesVerifier(certificates);
  40. }
  41. // 调用下载证书的方法
  42. downloadAndUpdateCert(verifier);
  43. }
  1. // 初始化证书方法
  2. private void initCertificates() {
  3. downloadAndUpdateCert(null);
  4. }
  5. // 线程安全的证书下载方法
  6. private synchronized void downloadAndUpdateCert(Verifier verifier) {
  7. // 创建出httpClient对象
  8. try (CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
  9. .withCredentials(credentials)
  10. .withValidator(verifier == null ? (response) -> true
  11. : new WechatPay2Validator(verifier))
  12. .build()) {
  13. // 发送GET请求设置请求通信息
  14. HttpGet httpGet = new HttpGet(CERT_DOWNLOAD_PATH);
  15. httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString());
  16. // 发送请求
  17. try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
  18. // 获取结果code
  19. int statusCode = response.getStatusLine().getStatusCode();
  20. // 获取结果数据
  21. String body = EntityUtils.toString(response.getEntity());
  22. // 如果结果的 200
  23. if (statusCode == SC_OK) {
  24. // 将证书文件解码后存放到map中
  25. Map<BigInteger, X509Certificate> newCertList = CertSerializeUtil.deserializeToCerts(apiV3Key, body);
  26. if (newCertList.isEmpty()) {
  27. log.warn("Cert list is empty");
  28. return;
  29. }
  30. // 清除所有的证书
  31. certificates.clear();
  32. // 重新添加证书
  33. certificates.putAll(newCertList);
  34. } else {
  35. log.error("Auto update cert failed, statusCode = {}, body = {}", statusCode, body);
  36. }
  37. }
  38. } catch (IOException | GeneralSecurityException e) {
  39. log.error("Download Certificate failed", e);
  40. }
  41. }

总结:

        证书每隔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 进行证书区分。
    1. 200: OK
    2. {
    3. "data": [
    4. {
    5. "serial_no": "5157F09EFDC096DE15EBE81A47057A7232F1B8E1",
    6. "effective_time ": "2018-06-08T10:34:56+08:00",
    7. "expire_time ": "2018-12-08T10:34:56+08:00",
    8. "encrypt_certificate": {
    9. "algorithm": "AEAD_AES_256_GCM",
    10. "nonce": "61f9c719728a",
    11. "associated_data": "certificate",
    12. "ciphertext": "sRvt… "
    13. }
    14. },
    15. {
    16. "serial_no": "50062CE505775F070CAB06E697F1BBD1AD4F4D87",
    17. "effective_time ": "2018-12-07T10:34:56+08:00",
    18. "expire_time ": "2020-12-07T10:34:56+08:00",
    19. "encrypt_certificate": {
    20. "algorithm": "AEAD_AES_256_GCM",
    21. "nonce": "35f9c719727b",
    22. "associated_data": "certificate",
    23. "ciphertext": "aBvt… "
    24. }
    25. }
    26. ]
    27. }

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的具体内容):
    1. HTTP/1.1 200 OK
    2. Server: nginx
    3. Date: Tue, 02 Apr 2019 12:59:40 GMT
    4. Content-Type: application/json; charset=utf-8
    5. Content-Length: 2204
    6. Connection: keep-alive
    7. Keep-Alive: timeout=8
    8. Content-Language: zh-CN
    9. Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
    10. Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c // 应答随机串
    11. // 应答签名
    12. Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
    13. Wechatpay-Timestamp: 1554209980 // 应答时间戳
    14. Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
    15. Cache-Control: no-cache, must-revalidate
    16. // 应答报文主体
    17. {"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":"..."}}]}

    • 则验签名串为
      1. 1554209980
      2. c5ac7061fccab6bf3e254dcf98995b8c
      3. {"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 进行执行验证操作。

  1. package com.wechat.pay.contrib.apache.httpclient.auth;
  2. import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.REQUEST_ID;
  3. import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_NONCE;
  4. import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL;
  5. import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SIGNATURE;
  6. import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_TIMESTAMP;
  7. import com.wechat.pay.contrib.apache.httpclient.Validator;
  8. import java.io.IOException;
  9. import java.nio.charset.StandardCharsets;
  10. import java.time.DateTimeException;
  11. import java.time.Duration;
  12. import java.time.Instant;
  13. import org.apache.http.Header;
  14. import org.apache.http.HttpEntity;
  15. import org.apache.http.client.methods.CloseableHttpResponse;
  16. import org.apache.http.util.EntityUtils;
  17. import org.slf4j.Logger;
  18. import org.slf4j.LoggerFactory;
  19. /**
  20. * @author xy-peng
  21. */
  22. public class WechatPay2Validator implements Validator {
  23. protected static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
  24. /**
  25. * 应答超时时间,单位为分钟
  26. */
  27. protected static final long RESPONSE_EXPIRED_MINUTES = 5;
  28. // 验证器
  29. protected final Verifier verifier;
  30. public WechatPay2Validator(Verifier verifier) {
  31. this.verifier = verifier;
  32. }
  33. // 参数异常处理方法
  34. protected static IllegalArgumentException parameterError(String message, Object... args) {
  35. message = String.format(message, args);
  36. return new IllegalArgumentException("parameter error: " + message);
  37. }
  38. // 验证失败处理异常
  39. protected static IllegalArgumentException verifyFail(String message, Object... args) {
  40. message = String.format(message, args);
  41. return new IllegalArgumentException("signature verify fail: " + message);
  42. }
  43. // 核心方法验证方法
  44. @Override
  45. public final boolean validate(CloseableHttpResponse response) throws IOException {
  46. try {
  47. // 验证响应的结果参数是
  48. validateParameters(response);
  49. // 生成签名
  50. String message = buildMessage(response);
  51. // 获取微信平台证书序列号
  52. String serial = response.getFirstHeader(WECHAT_PAY_SERIAL).getValue();
  53. // 获取当前应答签名
  54. String signature = response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue();
  55. // 验签比较方法,
  56. if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
  57. throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
  58. serial, message, signature, response.getFirstHeader(REQUEST_ID).getValue());
  59. }
  60. } catch (IllegalArgumentException e) {
  61. log.warn(e.getMessage());
  62. return false;
  63. }
  64. return true;
  65. }
  66. // 校验参数方法
  67. protected final void validateParameters(CloseableHttpResponse response) {
  68. // 获取第一个请求为 Request-ID
  69. Header firstHeader = response.getFirstHeader(REQUEST_ID);
  70. // 如果请求头为null则验证失败
  71. if (firstHeader == null) {
  72. throw parameterError("empty " + REQUEST_ID);
  73. }
  74. // 获取请求id
  75. String requestId = firstHeader.getValue();
  76. // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
  77. // 组装一个 响应的参数名称集合 有 证书序列号、应答签名、时间戳、随机字符串
  78. String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
  79. Header header = null;
  80. // 遍历所需要的请求的参数名取从应答对象中拿如果没有则报错
  81. for (String headerName : headers) {
  82. // 拿去所需要的应答对象数据
  83. header = response.getFirstHeader(headerName);
  84. if (header == null) {
  85. throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
  86. }
  87. }
  88. // 因为最后的值是时间戳,所以这里直接获取到时间戳了
  89. String timestampStr = header.getValue();
  90. try {
  91. // 将时间戳进行转换为时间
  92. Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
  93. // 比较应答时间和当前时间超过默认5分钟, 拒绝过期应答
  94. if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
  95. throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
  96. }
  97. } catch (DateTimeException | NumberFormatException e) {
  98. throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
  99. }
  100. }
  101. // 验签名串
  102. protected final String buildMessage(CloseableHttpResponse response) throws IOException {
  103. String timestamp = response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue();
  104. String nonce = response.getFirstHeader(WECHAT_PAY_NONCE).getValue();
  105. String body = getResponseBody(response);
  106. return timestamp + "\n"
  107. + nonce + "\n"
  108. + body + "\n";
  109. }
  110. // 获取报文主体结果
  111. protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
  112. HttpEntity entity = response.getEntity();
  113. return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
  114. }
  115. }
  • 真正验签会走到 CertificatesVerifier 的 verify 方法
  •  certificates.get(val) 是根据 微信平台证书序列号从map中获取证书信息
  • 获取到验证器会调用 verify 进行组装 参数 最终调用 Signature 的 verify 进行验证
  1. protected boolean verify(X509Certificate certificate, byte[] message, String signature) {
  2. try {
  3. // 创建签名器,并指定签名算法
  4. Signature sign = Signature.getInstance("SHA256withRSA");
  5. sign.initVerify(certificate); // 设置签名器使用的证书公钥
  6. sign.update(message);// 要验证的数据
  7. // 验证传入的签名数据 也就是 update 和 verify 数据进行比较
  8. return sign.verify(Base64.getDecoder().decode(signature));
  9. } catch (NoSuchAlgorithmException e) {
  10. throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
  11. } catch (SignatureException e) {
  12. throw new RuntimeException("签名验证过程发生了错误", e);
  13. } catch (InvalidKeyException e) {
  14. throw new RuntimeException("无效的证书", e);
  15. }
  16. }

 ​​​​​​​

 

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

2.总结:

e1999672bb7e9be8fb9d5748193f9594.png

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

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

2.将数据生成签名串

3.获取应答签名串

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

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/314425
推荐阅读
相关标签
  

闽ICP备14008679号