微信支付平台证书自动更换

微信支付平台证书自动更换

本文记录作者本人开发过程中遇见的问题,最后手写实现 和 APIv3 Java SDK 自动更换都得到了可行性验证。
不过只是在本地运行过了,并没有经过测试生产验证,所以引用者注意一下。

1. 关键字概述

在写代码之前,我们应该明白微信平台相关的一些概念

  • mchntId : 首先需要成为微信支付商户,得到 mchntId
  • mchntPrivateKey:商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。商户给微信发送请求时,需要用到商户自己的私钥加签,微信收到请求后,用商户的公钥进行验签,来验证请求合法性。那么公钥在哪?是根据下面这个商户的序列号来获得的。
  • mchntPubSerialNo:指由商户申请的证书所包含的证书序列号。商户给微信发送请求,需要在请求头里携带这个序列号,它就相当于储存商户基本信息的主键,微信根据这个序列号,可以获取到商户的证书、里面包含商户的商户号、公司名称、公钥信息等不敏感的信息。
  • APIv3Key:为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了 AES-256-GCM 加密。APIv3 密钥是加密时使用的对称密钥。

以上信息为开发前必备的信息

  • weChatPrivateKey 微信平台自己的私钥,商户给微信发送请求后,微信会回调一个请求给商户端,这个请求也会加签,用的就是微信自己的私钥。
  • weChatPublicKey : 微信平台自己的公钥,他就在我们要下载的 微信证书 里面解析出来的。 微信证书 是本篇文章要获取的东西。他主要有两个作用
    1. 商户收到微信的回调请求或者其他请求,商户肯定需要验签,来判断这个请求是不是微信发过来的。
    1. 对商户发送的敏感信息进行加密,微信收到后,微信用平台自己的私钥解密。是的。你没看错,之前的信息都是私钥加密,公钥解密,这里是公钥加密,私钥解密。因为我们不可能拿到微信平台的私钥。
  • mchntPubSerialNo : 微信平台自己的证书序列号,也是本篇文章要获取的东西;微信的序列号和证书可能同时存在多个,给微信发送请求需要上传这个微信平台序列号,微信才知道你用的是哪一套证书。
  • 公钥与证书关系 : 由于公钥本身并不含有拥有者的身份信息,使用时无法确认它是真实有效的。所以需要证书认证机构在核实公钥拥有者的信息后,将公钥拥有者的身份信息(如商户号、公司名称等),公钥、签发者信息、有效期以及扩展信息等进行签名,制作成“证书”。
  • .p12文件、.pem文件 : 初次下载的包里包含一个后缀名是p12的文件和两个后缀名是 pem的文件,一般p12文件可以解析出来两个pem文件,我们在项目里配置pem文件的路径io流处理就行。

加签验签安全机制,详见微信平台

2. 自动更换证书背景

我们只有先了解他的执行流程,才能往下开发;

1. 为什么要换证书?

微信平台证书有效期五年换一次,那么新旧证书如何平滑的替换让客户无感知,万一旧证书过期了,新证书还没有部署怎么办,就出了生产事故,还是涉及到资金的事故。

2. 微信平台新旧平台证书更换机制

在这里插入图片描述

3. 更新平台证书方法

  1. 定时更新:定时获取微信支付平台证书。更新后,自动将证书部署到生产环境中。
  2. 惰性加载:当商户应用程序中无证书序列号对应证书时,调用API获取对应平台证书,并缓存以供验签使用。惰性加载实现简单,但会增加处理耗时和重复下载,仅建议请求量较少、对用户访问延时要求不高的商户选用。
  3. 运维手搓,商户收到过期短信后,靠开发运维人员手动更换,反正五年一次。但是多个商户部署的时候,更换很频繁,换了还要重启,烦烦烦

前两种为微信平台推荐,最推荐的肯定是第一种,
至于最后一种,反正互联网公司的开发员人也很少能干满五年,倒也不是不行,哈哈哈哈

4. 流程图

看这个画的挺好的,引用过来

在这里插入图片描述

3. 开发-快速开始

1. 引包

0.2.3 版本以上才支持 RSAAutoCertificateConfig 自动下载 , 相应的JDK也需要1.8+;

<dependency>
  <groupId>com.github.wechatpay-apiv3</groupId>
  <artifactId>wechatpay-java</artifactId>
  <version>0.2.14</version>
</dependency>

2.更换微信平台证书步骤

成为微信支付商户,获取必要信息
🔰
构造签名串
🔰
调用下载平台证书接口
🔰
解密,获取证书
🔰
验证回调签名

3. 自己的代码实线

定时任务


public void updateWechatCertificate() {

        String token = RsaKit.getToken("GET", "https://api.mch.weixin.qq.com/v3/certificates", "", merchantId, serialNumber, privateKeyPath);
        //获取请求的返回结果
        CloseableHttpResponse responseResult = HttpClientUtil.getJson(certUrl, token);
        /**
         * 序列号对比,不同等待下一次更新
         */
        String serial_no = responseResult.getFirstHeader(WECHAT_PAY_SERIAL).getValue();
        /*if (!pubSerialNo.equals(serial_no)) {
            log.error("序列号:{},获取证书序列号与商户存储平台序列号不一致!", serial_no);
            return;
        }*/
        String bodyStr = RsaKit.getResponseBody(responseResult);
        log.error("获取微信证书返回body:{}", bodyStr);
        Map<String, String> stringMap = JsonUtils.readValue(bodyStr, Map.class);
        if (stringMap == null || stringMap.get("data") == null) {
            log.error("获取微信证书失败:{},失败原因:{}", stringMap.get("code"), stringMap.get("message"));
        } else {
            DownloadCertificateResponse response = JsonUtils.readValue(JsonUtils.toJsonStr(stringMap), DownloadCertificateResponse.class);
            List<Data> dataList = response.getData();
            if (CollUtil.isNotEmpty(dataList)) {
                SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
                dataList.stream().forEach(data -> {
                    String effectiveTime = data.getEffectiveTime();
                    String expireTime = data.getExpireTime();
                    try {
                        if (DateUtil.isIn(new Date(), inputFormat.parse(effectiveTime), inputFormat.parse(expireTime))) {
                            EncryptCertificate certificate = data.getEncryptCertificate();

                            try {
                                String cert = RsaKit.decryptToString(certificate.getAssociatedData().getBytes(StandardCharsets.UTF_8), certificate.getNonce().getBytes(StandardCharsets.UTF_8), certificate.getCiphertext(), apiV3Key);
                                log.info("解析出来的证书串为:{}", cert);
                                X509Certificate x509Certificate = getCertificate(cert);
                                boolean validate = RsaKit.validate(responseResult, bodyStr, x509Certificate);

                                if (validate) {
                                    log.info("序列号:{},验签成功!保存证书到相应路径下", serial_no);
                                    // 将证书的文件名命名为 {商户号}_{过期时间}_{证书序列号}.pem
                                    String filePath = merchantCertificatePath + merchantId + "_" + inputFormat.parse(expireTime).getTime() + "_" + serial_no + ".pem";
                                    /**
                                     * 保存pem文件
                                     */
                                    savePublicPemFile(cert, filePath);
                                    /**
                                     * 保存数据至数据库字典表
                                     */
                                    savePublicInfoToDatabase(data.getSerialNo(), data, filePath);
                                } else {
                                    log.error("序列号:{},验签失败!", serial_no);
                                }
                            } catch (IOException e) {
                                log.error("验签处理失败!");
                                return;
                            } catch (GeneralSecurityException e) {
                                log.error("获取平台公钥失败!");
                                return;
                            }

                        }
                    } catch (ParseException e) {
                        log.error("揭秘证书处理失败!");
                    }
                });
            }
        }
    }

    private void savePublicInfoToDatabase(String serialNo, Data data, String filePath) {
        Dic dic = new Dic();
        dic.setType(DIC_KEY_CERTIFICATE_KEY);
        dic.setDicKey(serialNo);

        data.getEncryptCertificate().setCiphertext("-----太长了,请看文件-----");
        dic.setDicValue(JsonUtils.toJsonStr(data));
        dic.setRemark(filePath);
        dic.setCreated(LocalDateTime.now());
        sysDictService.save(dic);

    }

    /**
     * 保存pem证书
     *
     * @param key      证书内容
     * @param filename 文件路径 + 文件名
     * @throws IOException
     */
    private void savePublicPemFile(String key, String filename) throws IOException {
        FileWriter fileWriter = null;
        try {
            File file = new File(filename);
            if (!file.exists()) {
                try {
                    file.createNewFile();
                    fileWriter = new FileWriter(file);
                    fileWriter.write(key);
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                    return;
                }
            }
        } finally {
            if (null != fileWriter) {
                fileWriter.close();
            }
        }
    }

    /**
     * 获取证书
     *
     * @param publicKey
     * @return {@link X509Certificate} 获取证书
     */
    public static X509Certificate getCertificate(String publicKey) {
        InputStream inputStream = null;
        try {
            inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException e) {
            throw new RuntimeException("证书已过期", e);
        } catch (CertificateNotYetValidException e) {
            throw new RuntimeException("证书尚未生效", e);
        } catch (CertificateException e) {
            throw new RuntimeException("无效的证书", e);
        }
    }

HttpClientUtil

@Slf4j
public class HttpClientUtil {
    public static final String SunX509 = "SunX509";
    public static final String JKS = "JKS";
    public static final String PKCS12 = "PKCS12";
    public static final String TLS = "TLS";

    public static HttpURLConnection getHttpURLConnection(String strUrl)
            throws IOException {
        URL url = new URL(strUrl);
        HttpURLConnection httpURLConnection = (HttpURLConnection) url
                .openConnection();
        return httpURLConnection;
    }

    /**
     * 获取请求的返回结果
     *
     * @param url
     * @param token
     * @return
     */
    public static CloseableHttpResponse getJson(String url, String token) {
        CloseableHttpClient httpClient = buildHttpClient(url.startsWith("https"));
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Authorization", token);
        httpGet.addHeader("Accept", "*/*");
        httpGet.addHeader("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(10000).setConnectTimeout(30000).build();
        httpGet.setConfig(requestConfig);
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
        } catch (IOException e) {
            log.error("请求api出现异常", e);
        }
        return response;
    }

    private static CloseableHttpClient buildHttpClient(boolean isHttps) {
        if (!isHttps) {
            return HttpClients.createDefault();
        } else {
            return createSSLClient();
        }
    }

    private static String parseReponse(HttpResponse response) {
        HttpEntity entity = response.getEntity();
        String body = null;
        try {
            body = EntityUtils.toString(entity, StandardCharsets.UTF_8);
            log.info("response :{}", body);
        } catch (IOException | ParseException e) {
            log.error("解析应答出现异常", e);
        }
        return body;
    }


    private static CloseableHttpClient createSSLClient() {
        try {
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(
                    null, (chain, authType) -> true).build();
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                    sslContext,
                    SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
            return HttpClients.custom().setSSLSocketFactory(sslsf).build();
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
            e.printStackTrace();
        }
        return HttpClients.createDefault();
    }

}

工具类

public class RsaKit {

    /**
     * 加密算法RSA
     */
    private static final String ALGORITHM_TYPE = "RSA";

    public static String getToken(String method, String url, String body, String mchid, String serialNo, String privateKeyPath) {
        String schema = "WECHATPAY2-SHA256-RSA2048";
        HttpUrl httpurl = HttpUrl.parse(url);
        String nonceStr = ChannelUtil.getNonceStr();
        long timestamp = System.currentTimeMillis() / 1000;
        String message = buildMessage(method, httpurl, timestamp, nonceStr, body);
        String signature = sign(message, privateKeyPath);
        if (StringUtils.isNotBlank(signature)) {
            return schema + " mchid=\"" + mchid + "\","
                    + "nonce_str=\"" + nonceStr + "\","
                    + "timestamp=\"" + timestamp + "\","
                    + "serial_no=\"" + serialNo + "\","
                    + "signature=\"" + signature + "\"";
        }
        return null;
    }

    public static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
        String canonicalUrl = url.encodedPath();
        if (url.encodedQuery() != null) {
            canonicalUrl += "?" + url.encodedQuery();
        }
        String signString = method + "\n"
                + canonicalUrl + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
        return signString;
    }

    public static String sign(String message, String privateKeyPath) {
        String signatureStr = null;
        try {
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initSign(getPrivateKey(privateKeyPath));
            sign.update(message.getBytes("UTF-8"));
            signatureStr = java.util.Base64.getEncoder().encodeToString(sign.sign());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (SignatureException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        }
        return signatureStr;
    }

    /**
     * 获取私钥。
     *
     * @param filename 私钥文件路径  (required)
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String filename) throws IOException {

        String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(new PKCS8EncodedKeySpec(java.util.Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }


    /**
     * ===============================验签===================================================
     */


    private static int RESPONSE_EXPIRED_MINUTES = 5;


    protected static IllegalArgumentException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }

    public static final boolean validate(CloseableHttpResponse response, String bodyStr, X509Certificate cert) {
        RsaKit.validateParameters(response);
        String message = buildMessage(response, bodyStr);
        System.out.println(message);
        String signature = response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue();

        return verify(cert, message.getBytes(StandardCharsets.UTF_8), signature);
    }


    protected static boolean verify(X509Certificate certificate, byte[] message, String signature) {
        System.out.println("signature: " + signature);
        try {
            Signature sign = Signature.getInstance("SHA256withRSA");
            sign.initVerify(certificate.getPublicKey());
            sign.update(message);
            return sign.verify(Base64Utils.decodeFromString(signature));

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

    public static final void validateParameters(CloseableHttpResponse response) {
        Header firstHeader = response.getFirstHeader(REQUEST_ID);
        if (firstHeader == null) {
            throw parameterError("empty " + REQUEST_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));
            // 拒绝过期应答
            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);
        }
    }

    public static final String buildMessage(CloseableHttpResponse response, String bodyStr) {
        String timestamp = response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue();
        String nonce = response.getFirstHeader(WECHAT_PAY_NONCE).getValue();
        return timestamp + "\n"
                + nonce + "\n"
                + bodyStr + "\n";
    }

    public static final String getResponseBody(HttpResponse response) {
        HttpEntity entity = response.getEntity();
        String body = null;
        try {
            body = EntityUtils.toString(entity, StandardCharsets.UTF_8);
        } catch (IOException | org.apache.http.ParseException e) {
        }
        return body;
    }

    /**
     * ==========================================解密=============================================================
     */

    static final int TAG_LENGTH_BIT = 128;

    public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext, String apiV3Key) throws GeneralSecurityException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);

            return new String(cipher.doFinal(java.util.Base64.getDecoder().decode(ciphertext)), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }

代码虽然最后获取到了证书,但是也有很多情况没有考虑

  1. 考虑多个商户号的情况;
  2. 多机部署服务;
  3. 有多个证书同时存在的时候,会在表或者缓存中存储多个证书,拿到平台序列号再去找对应的证书进行验签。
  4. 敏感信息加密如何选择最新的那个证书。
  5. 一般服务器都部署在内网,需要找一个代理服务器进行与外网互动,要不然微信获取证书列表的接口请求不出去。
  6. 有人报 Illegal key size 异常,详见:解决方案

4. 微信平台自动更换

主要类 RSAAutoCertificateConfig ,RSAAutoCertificateConfig 会利用 AutoCertificateService 自动下载微信支付平台证书。 AutoCertificateService 将启动一个后台线程,定期(目前为每60分钟)更新证书,以实现证书过期时的平滑切换。在每次构建 RSAAutoCertificateConfig 时,SDK 首先会使用传入的商户参数下载一次微信支付平台证书。 如果下载成功,SDK 会将商户参数注册或更新至 AutoCertificateService。若下载失败,将会抛出异常。

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

    /**
     * 定义商户的全局配置信息,要求一个商户号对应一个配置
     * 不能重复生成配置
     * 为了提高性能,建议将配置类作为全局变量。 复用 RSAAutoCertificateConfig
     * 可以减少不必要的证书下载,避免资源浪费。 只有在配置发生变更时,
     * 才需要重新构造 RSAAutoCertificateConfig。
     */
    public static Config getInstance(WxPayConfig wxPayConfig){
        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())
                    .merchantSerialNumber(wxPayConfig.getMchSerialNo())
                    .apiV3Key(wxPayConfig.getApiV3Key())
                    .build();
        }catch (Exception e){
            e.printStackTrace();
            log.error("构建商户配置信息出错,错误信息为"+e.getMessage());
            return null;
        } 
        return instance;
    }

我们来看new RSAAutoCertificateConfig.Builder()做了什么
在这里插入图片描述
RSAAutoCertificateProvider里,拼装商户的签名信息,构造httpClient,然后就是返回构造证书下载器
在这里插入图片描述
证书下载器,调用的AutoCertificateService这个工具类里面的register注册证书下载任务

注册证书下载任务 如果是第一次注册,会先下载证书。如果能成功下载,再保存下载器,供定时更新证书使用。如果下载失败,会抛出异常。
如果已经注册过,当前传入的下载器将覆盖之前的下载器。如果当前下载器不能下载证书,定时更新证书会失败。

在这里插入图片描述
跳到AutoCertificateService里,它主要就是一个定时更新证书的服务,一个由静态函数构成的工具类
在这里插入图片描述
这里主要是启动一个线程池,然后下载证书,放在一个map里面ConcurrentHashMap<String, Map<String, X509Certificate>> certificateMap
start 就是启动了一个定时器ScheduledThreadPoolExecutor,一小时跑一次。
ConcurrentHashMap<String, Runnable> downloadWorkerMap 存的是定时任务需要的下载器的信息。
最主要的就是这个download()下载证书方法了,调用的是CertificateDownloader.download()
在这里插入图片描述

到这里按理说还是上面3.2 描述的那几步
构造签名串 → 调用下载平台证书接口 → 解密,获取证书→ 验证回调签名

构造签名串那就是也得有这些

HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n
其实他就在httpClient.execute的 .addHeader(AUTHORIZATION, getAuthorization(httpRequest))
。getAuthorization 调用 getToken,具体不多说了。

然后就是从应答报文中解密证书

跟上述手写decryptToString差不多,他从httpResponse解析出来的各参数
然后循环遍历Data 放在 Map<String, X509Certificate> downloadCertMap 里面,有几个证书就放几个

之后是验签

跟上述手写的getCertificate 一个道理
微信SDK的validateCertPath() 调用 validator.validate(certPath, params); 这个就是JDK的方法了,我太菜,看不懂了

出来后就被放进了AutoCertificateService类certificateMap 中,至于检查平台证书序列号、验证重放攻击、构造验签名串等步骤,好像微信并没有做,他可能认为自己调用自己,没问题吧,但是之后比如你要调用小程序下单等接口时,微信(SDK V3)自己是做了这些步骤的。

4. 应用

之前我用了平台自动下载证书的方式,之后就陷入了一种思想,怎么把AutoCertificateService类certificateMap弄出来,我去手动验签,搞了好久,没搞出来。因为领导只让我做自动获取证书,其实我还真不知道后面要这个证书干嘛,很执着的要把它搞出来,哈哈哈哈

1. 证书应用-验签

当我们用了 RSAAutoCertificateConfig 来自动更换证书后,我们就不需要过度的关注他了,微信平台封装的很好,无论你是要在H5、app、还是小程序下单用,微信都封装好了用certificateMap里面的证书验签。

举个栗子

小程序下单接口: 
JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build(); 
response = service.prepayWithRequestPayment(request);

prepayWithRequestPayment → JsapiServiceExtension → jsapiService.prepay(request) → httpClient.execute → validateResponse(originalResponse);(这一步做检查平台证书序列号、验证重放攻击、构造验签名串等) ...→ certificateProvider.getCertificate ... →  certificateMap去找这里面的证书,形成闭环

在这里插入图片描述

2. 证书应用-加签

在第一节关键字概述中就提到了,平台证书包含的公钥是要对商户发送的敏感信息进行加密,微信收到后,微信用平台自己的私钥解密的。
微信官网
这里需要的是最新的那个,也就是过期时间最久的那个,因为我们要给微信解密留下时间,万一我们加完密,微信还没解密证书就过期,就出现了风险。那么我们该如何获取这个平台证书的序列号呢?

其实微信工具类AutoCertificateService中已经有了

// 获取最新的证书
AutoCertificateService.getAvailableCertificate(String merchantId, String type);
//根据证书获取微信平台序列号
PemUtil.getSerialNumber(X509Certificate certificate)

3. 如果部署在内网怎么办?

服务器部署在内网,里面这些微信封装的接口,肯定一个也调不通
从自动更新证书就要开始修改代理clientBuilder

// 设置下载自动更新证书时网络配置
DefaultHttpClientBuilder clientBuilder = 
    new DefaultHttpClientBuilder()
        .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 8080));

// 设置商户配置,并使用 httpClientBuilder 设置 HttpClient 所需的网络配置
Config config =
    new RSAAutoCertificateConfig.Builder()
        .merchantId(merchantId)
        .privateKeyFromPath(privateKeyPath)
        .merchantSerialNumber(merchantSerialNumber)
        .apiV3Key(apiV3key)
        .httpClientBuilder(clientBuilder)
        .build();

然后在调用下单接口的时候,延续

clientBuilder.config(config);
JsapiService service = new JsapiService.Builder()
        .httpclient(clientBuilder.build())
        .build();

至此完美,欢迎补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值