收藏
回答

v3接口规则的回调验签怎么知道使用的哪个平台证书?

看官方给的范例 回调通知是这个验签使用的verifier怎么获取呢?回调是全是密文的 如果要通过获取平台证书接口 是需要商户号的呀!!

回答关注问题邀请回答
收藏

3 个回答

  • Me
    Me
    2022-04-22

    我自己琢磨出来的,只不过我用的是微信wechatpay-apache-httpclient 0.4.4版本

    1.常见术语说明

    - 证书:也就是常规意义上的rsa证书,包括公钥和私钥,一般敏感信息(姓名、身份证、手机)等都是通过rsa进行加密解密

    - 秘钥:在微信官方文档叫做秘钥,在微信官方sdk里面叫做aesKey或apiKey,其实就是用于aes解密的密钥,一般的AEAD_AES_256_GCM都是通过aes加密解密

    - 商户API证书:商户从[微信支付](https://pay.weixin.qq.com/)申请的证书,在【账户中心】->【API安全】->【申请API证书】菜单申请,申请的是rsa证书(一般会有3个文件,第一个p12后缀文件,这个一般用不到,另外两个pem后缀的文件,是商户的rsa公钥和私钥),一般商户证书有5年有效期

    - 商户API V3秘钥:注意不是上面商户API证书的私钥,而是商户在【账户中心】->【API安全】->【设置APIv3密钥 】菜单设置的秘钥,这个是32 byte的字符串(ascii字符就行,不建议使用特殊字符)

    - 平台证书:在微信官方文档也叫做“微信支付平台证书”,也就是微信官方使用的rsa公钥(私钥是微信官方持有),这个公钥比较特殊,需要我们使用接口下载,而且这个rsa公钥可能会到期,需要我们定期下载,目前微信官方的(wechatpay-apache-httpclient v0.4.4)内置了定期更新微信支付平台证书的功能,所以我们一般不需要考虑微信支付平台证书过期的问题

    - 签名:微信支付的api在请求或者响应里面一般都需要进行签名,目前微信官方的(wechatpay-apache-httpclient v0.4.4)自动集成了签名的功能,我们直接使用即可

    2.快速接入开发

    2.1 商户配置类

    注意下方CertificatesManager如果需要注册多个商户号,则需要使用putMerchant方法,下面因为只有一个商户号,所以直接在里面putMerchant,如果有多个商户号,则需要针对每个不同商户号的证书、密钥做对应处理

    @Component
    @Slf4j
    public class WechatPayApiV3Config {
        /**
         * 商户号(我们自己的商户号,在微信支付服务商平台里面申请的商户号)
         */
        @Value("${wechat.merchant.id}")
        private String mchId;
        /**
         * apiv3证书密钥,(微信支付平台 -【账户中心】-【api安全】-【设置APIv3密钥 】这里设置的密钥)
         */
        @Value("${wechat.merchant.api.v3.key}")
        private String apiV3Key;
        /**
         * 商户api证书序列号 (微信支付平台 -【账户中心】-【api安全】-【申请API证书】这里申请的证书序列号)
         */
        @Value("${wechat.merchant.certificate.serial}")
        private String mchSerialNo;
    
    
        /**
         * 商户rsa私钥文本,就是微信支付平台 -【账户中心】-【api安全】-【申请API证书】最终下载下来的rsa证书私钥文件的内容
         */
        private static String privateKey;
        /**
         * 商户rsa私钥文本转成PrivateKey对象
         */
        public static PrivateKey merchantPrivateKey;
    
    
        static {
            try (InputStream in = WechatPayApiV3Config.class.getClassLoader().getResourceAsStream("wechatpayapiv3/apiclient_key.pem")) {
                byte[] bytes = in.readAllBytes();
                privateKey = new String(bytes, StandardCharsets.UTF_8);
                merchantPrivateKey = PemUtil.loadPrivateKey(privateKey);
            } catch (IOException e) {
                log.error("load api client key error: {}", ExceptionUtil.getMessage(e));
            }
        }
    
    
        /**
         * CertificatesManager利用了ConcurrentHashMap存储多个商户的证书和密钥信息,所以支持多个商户号使用
         *
         * @return
         * @throws GeneralSecurityException
         * @throws IOException
         * @throws HttpCodeException
         */
        @Bean(destroyMethod = "stop")
        public CertificatesManager certificatesManager() throws GeneralSecurityException, IOException, HttpCodeException {
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId,
                    new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));
    
    
            return certificatesManager;
        }
    
    
        /**
         * Verifier是通过CertificatesManager导出的,是严格关联到某一个商户号的,所以不同的商户号需要对应不同的Verifier,如果生产环境有多个商户号,
         * 则建议将不同商户号的Verifier缓存到Map里面,因为certificatesManager.getVerifier(mchId);每次都会创建一个新的Verifier对象,频繁创建对象不好
         *
         * @param certificatesManager
         * @return
         * @throws NotFoundException
         */
        @Bean
        public Verifier verifier(CertificatesManager certificatesManager) throws NotFoundException {
            return certificatesManager.getVerifier(mchId);
        }
    
    
        /**
         * CloseableHttpClient是微信支付的client,内置了http请求的签名以及签名解密验证功能,所以说我们一般无需手工处理签名的加密、解密、验证,
         * 而且CloseableHttpClient也是严格关联到某一个商户号的,所以不同的商户号需要对应不同的CloseableHttpClient,如果生产环境有多个商户号,
         * 则建议将不同商户号的CloseableHttpClient缓存到Map里面
         *
         * @param verifier
         * @return
         */
        @Bean
        public CloseableHttpClient wechatPayApiV3HttpClient(Verifier verifier) {
            return WechatPayHttpClientBuilder.create()
                    .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
                    .withValidator(new WechatPay2Validator(verifier))
                    .build();
        }
    }
    


    2.2 手工生成signature签名 & 手工验证签名

    我们发送请求给微信,都是需要对请求参数、url等进行签名,前面提到了微信的CloseableHttpClient内置了请求自动签名,响应签名解密验证,如果某些特殊情况下需要手工验证签名(例如:微信回调我们的服务)

    注意:微信支付的签名都是使用rsa加密解密的。

    2.2.1 商户侧生成签名

    商户侧生成签名是需要使用商户自己的私钥进行加密,详情可以参考(wechatpay-apache-httpclient v0.4.4)WechatPay2Credentials的getToken方法,在这个方法中会使用到PrivateKeySigner对请求信息进行签名,一般通过微信的CloseableHttpClient发送的请求,内部自动做好了请求签名、响应签名验证,所以无需我们手工操作签名

    2.2.2 商户侧验证签名

    只有那些微信主动回调我们接口的地方,这时候就需要我们主动验证签名的有效性,防止黑客伪装签名数据,微信的响应或者回调,都会在http头部增加这么几个属性Wechatpay-Serial、Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce,我们拿到请求或响应body内容,接着通过verify进行验证签名

        /**
         * 验证签名
         *
         * @param body
         * @param nonce
         * @param serial
         * @param timestamp
         * @param signature
         * @return
         */
        public boolean verify(String body, String nonce, String serial, String timestamp, String signature) {
            NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(serial)
                    .withNonce(nonce)
                    .withTimestamp(timestamp)
                    .withSignature(signature)
                    .withBody(body)
                    .build();
    
            return verifier.verify(request.getSerialNumber(), request.getMessage(), request.getSignature());
        }
    


    2.3 数据加密解密

    2.3.3 AEAD_AES_256_GCM解密

        /**
         * aes解密
         *
         * @param nonce
         * @param associatedData
         * @param ciphertext
         * @return
         */
        public String aesDecrypt(String nonce, String associatedData, String ciphertext) throws GeneralSecurityException {
            AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(UTF_8));
            return aesUtil.decryptToString(associatedData.getBytes(UTF_8), nonce.getBytes(UTF_8), ciphertext);
        }
    

    2.3.3 敏感信息加密

    发送给微信的敏感信息,需要使用微信支付平台证书(rsa公钥)进行加密,加密之后,还需要在请求头增加一个Wechatpay-Serial的头部,这个头部就是微信支付平台证书的序列号(如何拿到序列号参考下列代码)

    //微信支付平台证书(rsa公钥),在CertificatesManager底层会定期更新的
    X509Certificate certificate = verifier.getValidCertificate();
    //拿到证书序列号(10进制的内容)
    BigInteger serialNumber = certificate.getSerialNumber();
    //转成16进制字符串
    String wechatPaySerial = serialNumber.toString(16).toUpperCase();
    //发送给微信的敏感信息加密
    String encrypt = RsaCryptoUtil.encryptOAEP("明文内容", certificate);
    HttpPost post = new HttpPost(url);
    String requestBody = JSONObject.toJSONString(request);
    StringEntity entity = new StringEntity(requestBody, APPLICATION_JSON);
    post.setEntity(entity);
    //设置请求头部Wechatpay-Serial,注意这里是微信支付平台证书序列号,不是商户证书序列号,如果用了商户证书序列号则微信会返回{"code":"PARAM_ERROR","message":"平台证书序列号Wechatpay-Serial错误"}
    post.addHeader(WechatPayHttpHeaders.WECHAT_PAY_SERIAL, wechatPaySerial);
    
    //设置请求头部ACCEPT,需要设置accept请求头,否则微信api会报错 {"code":"INVALID_REQUEST","message":"头部信息不完整"}
    post.addHeader(ACCEPT, APPLICATION_JSON.toString());
    CloseableHttpResponse response = wechatPayApiV3HttpClient.execute(post);
    int statusCode = response.getStatusLine().getStatusCode();
    byte[] bytes = response.getEntity().getContent().readAllBytes();
    String content = new String(bytes, UTF_8);
    


    2.3.4 敏感信息解密

    微信返回响应或者回调我们接口的敏感信息,微信使用了商户的公钥进行加密,所以我们自己需要用商户私钥解密

    //解密密文信息
    
    String message = RsaCryptoUtil.decryptOAEP("密文信息", WechatPayApiV3Config.merchantPrivateKey)
    


    2022-04-22
    有用
    回复 2
    • Me
      Me
      2022-04-22
      解释一下微信支付平台证书,这个证书在certificateManager初始化的时候,会自动下载,我们只需要通过X509Certificate certificate = verifier.getValidCertificate();就能拿到微信支付平台证书(rsa公钥)
      2022-04-22
      回复
    • Memory
      Memory
      2022-04-22回复Me
      人家不想自己写,要sdk自动处理
      2022-04-22
      回复
  • 。
    2022-04-22

    通过证书序列号 怎么获取平台证书信息?

    2022-04-22
    有用
    回复
  • Memory
    Memory
    2022-04-22

    通过请求头的证书序列号判断

    2022-04-22
    有用
    回复 14
    • 。
      2022-04-22
      蔡永钢~北京商联刚刚
      通过证书序列号 怎么获取平台证书信息?
      2022-04-22
      回复
    • 。
      2022-04-22
      sdk中有通过序列号获取平台证书的方法?
      2022-04-22
      回复
    • Memory
      Memory
      2022-04-22回复
      你自己下载了平台证书不会保存吗
      2022-04-22
      回复
    • 。
      2022-04-22回复Memory
      会保存的 使用的自动管理证书方法、但看这个方法想获取对应的证书 要通过商户号进行获取的
      2022-04-22
      回复
    • Memory
      Memory
      2022-04-22回复
      然后呢?
      2022-04-22
      回复
    查看更多(9)
登录 后发表内容