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

微信支付 文章 2022-04-22 14:40 0 全屏看文

v3接口规则的回调验签怎么知道使用的哪个平台证书?How does the callback signature verification of the v3 interface rule know which platform certificate to use?

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

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

Look at the example callback notice given by the official. How can the verifier used for signature verification be obtained? The callback is full of ciphertext. If you want to obtain the platform certificate interface, you need the merchant number!!

回答:

GCMG:

我自己琢磨出来的,只不过我用的是微信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@Slf4jpublic 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)


GCMG:解释一下微信支付平台证书,这个证书在certificateManager初始化的时候,会自动下载,我们只需要通过X509Certificate certificate = verifier.getValidCertificate();就能拿到微信支付平台证书(rsa公钥)
蔡永钢~北京商联:

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

Memory:

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

蔡永钢~北京商联:蔡永钢~北京商联刚刚
通过证书序列号 怎么获取平台证书信息?
蔡永钢~北京商联:sdk中有通过序列号获取平台证书的方法?
Memory:你自己下载了平台证书不会保存吗
蔡永钢~北京商联:会保存的 使用的自动管理证书方法、但看这个方法想获取对应的证书 要通过商户号进行获取的
Memory:然后呢?
蔡永钢~北京商联:回调时只能拿到微信侧平台证书序列号呀 多个商户时怎么知道是哪个商户号的呢?
Memory:自己都知道保存了,不会记录匹配?
蔡永钢~北京商联:保存是微信提供的sdk自行处理的
Memory:你不会自己写吗?
蔡永钢~北京商联:自己调用自己保存肯定可以 现在想使用微信的sdk自行处理证书,想通过证书序列号获取证书 有没有相应的方法?
蔡永钢~北京商联:我自己可以写呀 但是就是想问一下开发社区有没有 直接使用微信提供的 sdk进行使用的呀!!你不知道可以不回复
蔡永钢~北京商联:没人求你回复呀 如果有sdk提供的方法使用不是更好吗 你在这怼什么呢!!
Memory:
Memory:哟,急眼了?伤你自尊了?

-EOF-