Section 5, Project Payment Function Practice - Certificate Obtaining, WeChat Payment Integration Initialization Configuration, SDK Unified Order Placement, API Security Source Code Interpretation

Summary

This section will first explain the acquisition of merchant certificates, private keys, WeChat platform certificates, and the generation of APIv3 keys. Then initialize the parameter configuration information required by our WeChat payment for use in subsequent business codes. Combined with the WeChat platform certificate download case and the WeChat unified ordering API, we will explain what operations are performed on the request and response. How are the certificates we mentioned in the previous section used in the interface. Finally, let’s talk about how to penetrate internal and external networks.

Certificate acquisition

Obtain WeChat platform certificate

To ensure security during the API request process, the client needs to use the WeChat payment platform certificate to verify the authenticity and integrity of the server response. We need to download the WeChat platform certificate to use it. The main application scenario is for merchants to verify signatures and WeChat to decrypt the data. The server-side SDK has provided us with an API for downloading certificates. Open the source code address of the server sdk: https://github.com/wechatpay-apiv3/wechatpay-java . We can obtain the WeChat platform certificate by manually or automatically downloading the certificate. If we use the automatic download certificate from v0. Starting from version 2.3, a configuration class named RSAAutoCertificateConfig was introduced in the sdk to automatically update the platform certificate.

RSAAutoCertificateConfigWill use to AutoCertificateServiceautomatically download the WeChat payment platform certificate. AutoCertificateServiceA background thread will be started to update the certificate periodically (currently every 60 minutes) to allow for a smooth switch when the certificate expires. During each build RSAAutoCertificateConfig, the SDK will first download the WeChat payment platform certificate using the incoming merchant parameters. If the download is successful, the SDK will register or update the merchant parameters to AutoCertificateService. If the download fails, an exception will be thrown.

To improve performance, it is recommended to make the configuration class a global variable. Reuse RSAAutoCertificateConfigcan reduce unnecessary certificate downloads and avoid resource waste. Reconstruction is only required when the configuration changes RSAAutoCertificateConfig. If you have multiple merchant numbers, you can build one for each merchant RSAAutoCertificateConfig. In order to ensure that it is globally unique, let's initialize a global configuration class. We use singleton mode to automatically update the certificate code as follows: Create a WxInitUtils.java file and add the following code:

//商户的全局配置类
    private static Config instance;                                             

    /**
     * 定义商户的全局配置信息,要求一个商户号对应一个配置
     * 不能重复生成配置
     * RSAAutoCertificateConfig 会利用 AutoCertificateService 自动下载微信支付平台证书。
     * AutoCertificateService 将启动一个后台线程,定期(目前为每60分钟)更新证书,
     * 以实现证书过期时的平滑切换。
     * 在每次构建 RSAAutoCertificateConfig 时,
     * SDK 首先会使用传入的商户参数下载一次微信支付平台证书。 如果下载成功,SDK 会将商户参数注册或更
     * 新至 AutoCertificateService。若下载失败,将会抛出异常。
     * 为了提高性能,建议将配置类作为全局变量。 复用 RSAAutoCertificateConfig
     * 可以减少不必要的证书下载,避免资源浪费。 只有在配置发生变更时,
     * 才需要重新构造 RSAAutoCertificateConfig。
     */
    public static Config getInstance(WxPayConfig wxPayConfig){
    
    
        if (instance == null){
    
    
            //如果实例不存在,创建一个新的实例
            synchronized (WxPayConfig.class){
    
    
                //双重检查锁定,防止多线程竞争时创建多个实例
                if (instance == null){
    
    
                    try{
    
    
                        if(wxPayConfig == null){
    
    
                            log.info("配置信息加载出错===");
                            return null;
                        }
                        log.info("商户号为==="+ wxPayConfig.getMchId());
                        log.info("商户私钥串为==="+ wxPayConfig.getPrivateKey());
                        log.info("序列号为==="+ wxPayConfig.getMchSerialNo());
                        log.info("密钥为==="+ wxPayConfig.getApiV3Key());
                        instance =  new RSAAutoCertificateConfig.Builder()
                                .merchantId(wxPayConfig.getMchId())
                                .privateKey(wxPayConfig.getPrivateKey())
//                                .privateKeyFromPath(wxPayConfig.getPrivateKeyPath())
                                .merchantSerialNumber(wxPayConfig.getMchSerialNo())
                                .apiV3Key(wxPayConfig.getApiV3Key())
                                .build();
                    }catch (Exception e){
    
    
                        e.printStackTrace();
                        log.error("构建商户配置信息出错,错误信息为"+e.getMessage());
                        return null;
                    }
                }
            }
        }
        return instance;
    }

The above code instance instance will only be initialized once. Call the getInstance method and pass in the certificate information we applied for to complete the automatic update and download of the certificate.

Next, let's analyze the certificate download process from the perspective of source code analysis. The main purpose of analyzing the download process is to see how the key certificate in the previous section is applied. Let’s focus on analyzing this code block:

 instance =  new RSAAutoCertificateConfig.Builder()               
                                .merchantId(wxPayConfig.getMchId())
                                .privateKey(wxPayConfig.getPrivateKey())
                                .merchantSerialNumber(wxPayConfig.getMchSerialNo())
                                .apiV3Key(wxPayConfig.getApiV3Key())
                                .build();

Use new RSAAutoCertificateConfig.Builder() to get its internal static class Builder. The Builder class inherits the abstract class AbstractRSAConfigBuilder, and calls the method of the parent class to initialize the merchant information and put it in the AbstractRSAConfigBuilder configuration class. Call its internal build method to construct the RSAAutoCertificateProvider.Builder class, store the merchant information in the RSAAutoCertificateProvider, and call the build() method to download the certificate. The specific steps to download the certificate:
1. First rent and assemble the merchant's signature information (including the merchant certificate serial number, merchant private key, merchant number) as follows:

       credential =
              new WechatPay2Credential(
                  requireNonNull(merchantId),
                  new RSASigner(requireNonNull(merchantSerialNumber), privateKey));
        }

2. Construct httpClient and set the merchant certificate and signature authenticator into httpclient.
3. Request the certificate download address of the WeChat platform, download the certificate, and construct a certificate downloader:

 CertificateDownloader downloader =
        new CertificateDownloader.Builder()
            .certificateHandler(rsaCertificateHandler)
            .downloadUrl(REQUEST_URL)
            .aeadCipher(aeadCipher)
            .httpClient(httpClient)
            .build();

Among them REQUEST_URL = "https://api.mch.weixin.qq.com/v3/certificates?algorithm_type=RSA" , aeadCipheris the APIv3 key. 构造的httpClient
4. Certificate download:

  /**
   * 注册证书下载任务 如果是第一次注册,会先下载证书。如果能成功下载,再保存下载器,供定时更新证书使用。如果下载失败,会抛出异常。
   * 如果已经注册过,当前传入的下载器将覆盖之前的下载器。如果当前下载器不能下载证书,定时更新证书会失败。
   *
   * @param merchantId 商户号
   * @param type 调用方自定义的证书类型,例如 RSA/ShangMi
   * @param downloader 证书下载器
   */
  public static void register(String merchantId, String type, CertificateDownloader downloader) {
    
    
    String key = calculateDownloadWorkerMapKey(merchantId, type);
    Runnable worker =
        () -> {
    
    
          Map<String, X509Certificate> result = downloader.download();
          certificateMap.put(key, result);
        };

    // 下载证书,以验证配置是正确的
    // 如果错误将抛出异常,fast-fail
    worker.run();
    // 更新配置
    downloadWorkerMap.put(key, worker);

    start(defaultUpdateInterval);
  }

Open the download method:

  /** 下载证书 */
  public Map<String, X509Certificate> download() {
    
            
    HttpRequest httpRequest =
        new HttpRequest.Builder()
            .httpMethod(HttpMethod.GET)
            .url(downloadUrl)
            .addHeader(Constant.ACCEPT, " */*")
            .addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue())
            .build();
    HttpResponse<DownloadCertificateResponse> httpResponse =
        httpClient.execute(httpRequest, DownloadCertificateResponse.class);

    Map<String, X509Certificate> downloaded = decryptCertificate(httpResponse);
    validateCertificate(downloaded);
    return downloaded;
  }

Please note here that the key point is coming. When downloading the certificate, the sdk encapsulates the signature into the httpClient. In the execute method in this code, the token value is constructed and the token is placed in the header of the request. Focus on this HttpResponse<DownloadCertificateResponse> httpResponse = httpClient.execute(httpRequest, DownloadCertificateResponse.class);method. getAuthorization(httpRequest)In the method, the merchant's private key is used to sign the request body, and the plain text, the merchant's public key, and the signature information are composed of a token string and sent to the WeChat platform. The specific code block to construct the token is:

 String token =
        "mchid=\""
            + getMerchantId()
            + "\","
            + "nonce_str=\""
            + nonceStr
            + "\","
            + "timestamp=\""
            + timestamp
            + "\","
            + "serial_no=\""
            + signature.getCertificateSerialNumber()
            + "\","
            + "signature=\""
            + signature.getSign()
            + "\"";

After the request is completed, the WeChat platform will return the response data in real time. Called on the merchant side decryptCertificate(httpResponse)to decrypt the response data, using the APIv3 key we set before when decrypting. Then the validity of the downloaded certificate will be verified and validateCertificate(downloaded);the specific verification code segment will be called as follows:

 PKIXParameters params = new PKIXParameters(trustAnchor);
      params.setRevocationEnabled(false);

      List<X509Certificate> certs = new ArrayList<>();
      certs.add(certificate);

      CertificateFactory cf = CertificateFactory.getInstance("X.509"); 
      CertPath certPath = cf.generateCertPath(certs);

      CertPathValidator validator = CertPathValidator.getInstance("PKIX");
      validator.validate(certPath, params);

The above code block is used to check whether the certificate is issued by a trusted root certificate and whether the certificate is valid. X509Certificate is a commonly used digital certificate standard. First create an object of type PKIXParameters, which is a parameter collection used to verify the certificate path. Its constructor requires a trustAnchor parameter, which represents a trusted root certificate. It is an object of Set type, which contains one or more root certificate information. getInstance("PKIX") returns a validator instance that supports the PKIX (Public Key Infrastructure) algorithm.

After the certificate download is completed, it will be stored in the AutoCertificateService class certificateMap. The certificateMap is a ConcurrentHashMap, which is thread-safe and can support concurrent access and modification of multiple threads. The AutoCertificateService class is a service that regularly updates certificates. It is composed of static functions. Tools.

private static final ConcurrentHashMap<String, Map<String, X509Certificate>> certificateMap =       
      new ConcurrentHashMap<>();
Map<String, X509Certificate> result = downloader.download();                       
certificateMap.put(key, result);

At this point, the automatic download process of the WeChat platform certificate is clearly described. Let’s summarize the specific process below:

(1) The merchant side packages merchant information: merchant number, merchant public key, merchant private key and other information, and signs the requested data. Pack the signature, public key, timestamp, random number and other information into a token and put it in the request header and send it to the WeChat platform.
(2) After receiving the request, WeChat Tester obtains the authentication token, parses out the merchant's public key and plain text information, and verifies the data. At the same time, the APIv3 key is used to encrypt and synchronize the certificate and return it to the merchant side.
(3) After receiving the response data from the WeChat side, the merchant side decrypts the certificate from the response data. After decrypting the response message, an X.509 certificate object is generated, that is, the certificate is converted from String to X509Certificate.
(4) Verify the validity of the certificate
(5) Store the certificate in the certificateMap in AutoCertificateService.

APIv3 key settings

In the merchant platform [WeChat Pay Merchant Platform - Account Center - Account Settings - API Security - APIv3 Key Settings], you can use the online generator to generate the address: https://www.bchrt.com/tools/suijimima/

Obtaining the merchant platform certificate and private key

The following content is from the WeChat official website: https://kf.qq.com/faq/161222NneAJf161222U7fARv.html
1. Log in to [WeChat Payment Merchant Platform - Account Center - Account Settings - API Security - Apply for API Certificate] to apply for a certificate. Please do not do so after confirmation. Close the page
Insert image description here
Insert image description here
2. Click to download the certificate tool; after downloading, double-click the "WXCertUtil.exe" file, select the installation path, and click Apply for Certificate

You can also download the certificate tool through the following link:

windows version
mac version

3. In [Certificate Tool], fill in the merchant number information (merchant number, merchant name), and click Next
Insert image description here
4. In [Certificate Tool], copy the certificate request string
(if prompted "Please paste the request string to the merchant platform to obtain the certificate string" , please check whether it has been pasted in step 5. You can also try the manual mouse copy and paste method)
Insert image description here
5. In [Merchant Platform], paste the certificate request string
Insert image description here
6. In [Merchant Platform], enter the operation password, and generate it after security verification Certificate string
Insert image description here
7. In [Merchant Platform], copy the certificate string
Insert image description here
8. In [Certificate Tool], paste the certificate string, click Next, and the certificate application is successful
(if it prompts "The certificate does not match the local public and private keys", it may be that the browser is disabled The clipboard copy function is enabled. Please use the mouse to select all the certificate string contents during the operation at point 7 (note that there is a drop-down box on the right), right-click the mouse and select copy)
Insert image description here
Insert image description here
Reminder: Please transfer the generated certificate file to the technical staff personnel, and technical personnel will deploy the certificate to the server (please be sure to keep the certificate and private key properly, because the private key file can only be exported through the certificate tool. If the private key is lost, it cannot be retrieved and can only be invalidated and reapplied.)

9. After the certificate application is successful, you will find three files when you unzip the file in the certificate folder: .p12. Don’t worry about this. Two of them are apiclient_cert.pem (merchant public key file) and apiclient_key (merchant private key file).

At this point, the merchant API certificate has been obtained.

Initialize WeChat configuration

In order to uniformly configure the information used for WeChat payment, we create a configuration file in the resource directory of the projectwxpay.properties

# 商户号
wxpay.mch-id=xxxxxxx
# 微信商户证书序列号                                                                           
wxpay.mch-serial-no=xxxxxxxxxxxxxxxxxxx
# 商户私钥(路径加载方式使用)
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=xxxxxxxxxxxxxx
# 小程序APPID
wxpay.appid=xxxxxxxxxxx
# 微信小程序密钥
wxpay.appSecret=xxxxxxxxxxxxxxxxxxxxxxxxx
# 微信商户平台域名(调用接口时的通用域名)
wxpay.domain=https://api.mch.weixin.qq.com
#微信支付回调地址域名(需要https 并且为备案的域名)
wxpay.notify-domain=https://test.notify.com
#商户私钥
wxpay.private-key=-----BEGIN PRIVATE KEY-----xxxx-----END PRIVATE KEY-----                                  

The corresponding configuration classes are as follows:

@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix="wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
public class WxPayConfig {
    
    

    // 商户号
    private  String mchId;

    // 商户API证书序列号
    private  String mchSerialNo;  

    // 商户私钥文件
    private  String privateKeyPath;

    // APIv3密钥
    private  String apiV3Key;

    // APPID
    private String appid;
    //小程序密钥
    private String appSecret;
    // 微信服务器地址
    private String domain;

    // 接收结果通知地址
    private String notifyDomain;

    // APIv2密钥
    private String partnerKey;

    //商户私钥字符串
    private String privateKey;
}

Unified order placement via mini program

This section mainly analyzes the case of unified ordering by a mini program to explain how to use SDK and how merchant certificates and platform certificates are used when calling the ordering interface. This section will not be too much in the current project. Expand the business code and only explain how to call the order interface and the security verification process.

Before calling the ordering interface of the mini program, you must first create the order data. When calling the ordering interface, pass the order data, such as order amount, product ID, order number, etc. After WeChat receives the order, it will return the prepaid order ID and the WeChat order information returned by the WeChat platform. Let's take a look at the specific calling code. The following is the code block for placing an order in the mini program.

  if (config == null){
    
             
            config = WxInitUtils.getInstance(wxPayConfig);
  }
 JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
        // 填充预下单参数
        PrepayRequest request = new PrepayRequest();
        //appid
        request.setAppid(wxPayConfig.getAppid());
        //商户id
        request.setMchid(wxPayConfig.getMchId());
        //产品描述
        assert orderInfo != null;
        request.setDescription(orderInfo.getOrderTitle());
        //商户订单号
        request.setOutTradeNo(orderInfo.getOrderNo());
        //通知url
        request.setNotifyUrl(wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));     

        //金额信息
        Amount amount = new Amount();
        amount.setTotal(orderInfo.getTotalFee());
        amount.setCurrency("CNY");
        request.setAmount(amount);

        //用户信息
        Payer payer = new Payer();
        payer.setOpenid(openId.toString());
        request.setPayer(payer);

        log.info("请求参数 ===> {}" + request.toString());
        PrepayWithRequestPaymentResponse response = null;

        try {
    
    
            // response包含了调起支付所需的所有参数,可直接用于前端调起支付
            response = service.prepayWithRequestPayment(request);
        } catch (Exception e) {
    
    
            log.error("请求下单失败,错误信息" + e.getMessage());
            throw new RuntimeException("请求下单失败,错误信息" + e.getMessage());
        }

        //处理返回值
        assert response != null;
        String packageVal = response.getPackageVal();
        String prepayId = packageVal;

In the above code, first get the configuration information config, then call JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();this is the api provided by the sdk, and then make a request to WeChat after assembling the parameters. The requested code segment response = service.prepayWithRequestPayment(request);calls String prepayId = this.jsapiService.prepay in the prepayWithRequestPayment method. (request).getPrepayId(); The real request is initiated in the prepay method. Let’s focus on the request code segment:

HttpRequest httpRequest = (new HttpRequest.Builder()).httpMethod(HttpMethod.POST).url(requestPath).headers(headers).body(this.createRequestBody(request)).build();
HttpResponse<PrepayResponse> httpResponse = this.httpClient.execute(httpRequest, PrepayResponse.class);

The following code is called in the execute method, which is the same as the interface call for downloading the certificate.

 HttpRequest innerRequest =
        new Builder()
            .url(httpRequest.getUrl())
            .httpMethod(httpRequest.getHttpMethod())
            .headers(httpRequest.getHeaders())
            .addHeader(AUTHORIZATION, getAuthorization(httpRequest))
            .addHeader(USER_AGENT, getUserAgent())
            .body(httpRequest.getBody())
            .build();
    OriginalResponse originalResponse = innerExecute(innerRequest);       
    validateResponse(originalResponse);

Focusing on Authorization, the composition is as follows:

Authorization: Authentication type signature information

The specific components are:
Authentication type: currently WECHATPAY2-SHA256-RSA2048
signature information
Merchant number of the merchant who initiated the request (including directly connected merchants, service providers or channel merchants) mchid
Merchant API certificate serial number serial_no, used to declare the certificate used
Request a random string nonce_str
timestamp timestamp
signature value signature

Examples are as follows:

‘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”’

The above is the signing process. The merchant's private key is used to sign. After signing, it is sent to the WeChat platform. Therefore, the mini program places orders uniformly. There is no encryption operation, only the signature operation. Similarly, after WeChat responds synchronously, it also needs to be verified on the merchant side.

The code segment for signature verification in SDK is as follows:

 String timestamp = responseHeaders.getHeader(WECHAT_PAY_TIMESTAMP);       
    try  {
    
    
      Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); 
      // 拒绝过期请求
      if (Duration.between(responseTime, Instant.now()).abs().toMinutes() 
          >= RESPONSE_EXPIRED_MINUTES) {
    
     
        throw new IllegalArgumentException(
            String.format(
                "Validate http response,timestamp[%s] of httpResponse is expires, " 
                    + "request-id[%s]", 
                timestamp, responseHeaders.getHeader(REQUEST_ID)));
      }
    } catch (DateTimeException | NumberFormatException e) {
    
     
      throw new IllegalArgumentException(
          String.format(
              "Validate http response,timestamp[%s] of httpResponse is invalid, request-id[%s]", 
              timestamp, responseHeaders.getHeader(REQUEST_ID)));
    }
    String message = 
        timestamp
            + "\n"
            + responseHeaders.getHeader(WECHAT_PAY_NONCE) 
            + "\n"
            + (responseBody == null ? "" : responseBody)
            + "\n";
    logger.debug("Message for verifying signatures is[{}]", message); 
    String serialNumber = responseHeaders.getHeader(WECHAT_PAY_SERIAL);
    logger.debug("SerialNumber for verifying signatures is[{}]", serialNumber); 
    String signature = responseHeaders.getHeader(WECHAT_PAY_SIGNATURE); 
    logger.debug("Signature for verifying signatures is[{}]", signature); 
    return verifier.verify(serialNumber, message, signature);

Verifying the signature requires the following steps:
(1) Verify whether the timestamp of the WeChat response has expired. The valid time is 5 minutes. // Reject the expired request
if (Duration.between(responseTime, Instant.now()).abs( ).toMinutes()
>= RESPONSE_EXPIRED_MINUTES) The purpose is to prevent replay attacks. Replay attacks refer to an attack method in which an attacker intercepts messages and their signatures and retransmits the data for malicious or fraudulent purposes. In order to reduce the risk of such attacks, WeChat Pay provides the timestamp for generating the signature in the HTTP header Wechatpay-Timestamp. If WeChat Pay needs to resend a notification callback, we will also regenerate the corresponding timestamp and signature. Before verifying the signature, the merchant system should check whether the timestamp has expired. We recommend that merchant systems allow up to 5 minutes of time deviation. If the timestamp deviates from the current time by more than 5 minutes, you should refuse to process the current response or callback notification.

(2) Construct a verification signature string.
The construction of a verification signature string consists of 3 parts. The following information is obtained in the response or notification callback: The
response timestamp in the HTTP header Wechatpay-Timestamp The
response random string in the HTTP header Wechatpay-Nonce
The body of the response message (Response Body), please use the original message body to perform signature verification. If you use a frame, make sure it does not tamper with the body of the message. Any tampering with the message body will cause verification to fail.

(3) Verify whether the WeChat platform certificate is correct.
Check whether the content of the HTTP header Wechatpay-Serial is consistent with the serial number of the Wechat payment platform certificate currently held by the merchant. If they are inconsistent, please obtain the certificate again. Otherwise, the signed private key and certificate do not match, and the verification will fail. The key code is as follows: Based on the certificate serial number returned in the header, check whether the certificate stored during downloading is consistent.

  /**
   * 根据证书序列号获取证书
   *
   * @param serialNumber 微信支付平台证书序列号
   * @return X.509证书实例
   */
  @Override
  public X509Certificate getCertificate(String serialNumber) {
    
    
    return AutoCertificateService.getCertificate(merchantId, ALGORITHM_TYPE, serialNumber);
  }

The above is the process of signing and verifying the order of the mini program. The complicated process is encapsulated in the SDK. We don’t need to pay too much attention to it, but we must be clear about the principle. It can be found that the order process is not encrypted. , the logic of decryption.

Guess you like

Origin blog.csdn.net/superzhang6666/article/details/135066263