【springboot进阶】RestTemplate 集成 okhttp3 请求带p12证书

目录

一、创建微信支付rest模板配置类

1、创建http请求工厂

2、实例化 RestTemplate 模板对象

二、封装微信支付相关的公共请求类

三、关于扩展

1、创建证书请求工厂方法

2、请求工厂的使用


由于最近在整合微信支付相关的接口,需要带上商户证书,而官方提供的 demo 都是老掉牙的版本,使用的 Apache 的 HttpClient,而现在基本上都是用的 SpringBoot 封装的 RestTemplate,所以记录一下这些坑是怎么踩过来的。

在之前的文章 《RestTemplate集成okhttp3并自定义日志打印》已经介绍过 okhttp3 的集成,所以这篇文章重点介绍怎么使用 okhttp3 请求带 p12 证书。

至于什么是 p12 证书,怎么从微信上导出 p12 证书,不在这篇文章介绍,大家可以自行查阅资料。

一、创建微信支付rest模板配置类

由于目前框架内已经有一个rest模板的配置类,那个配置类的请求是不带证书的,所以我们需要另外创建一个新的配置类,用来将我们的证书加载进去。

@Configuration
public class RestTemplateWechatCertConfig {

    @Bean
    @ConfigurationProperties(prefix = "org.liurb.core.rest-template.config.connection")
    public ClientHttpRequestFactory wechatHttpRequestFactory() throws Exception {

        KeyStore keyStore = KeyStore.getInstance("PKCS12");//eg. PKCS12

        ClassPathResource classPathResource = new ClassPathResource("wechat/apiclient_cert.p12");
        keyStore.load(classPathResource.getInputStream(), WechatAccountConfig.MCH_ID.toCharArray());

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, WechatAccountConfig.MCH_ID.toCharArray());

        SSLContext context = SSLContext.getInstance("TLS");
        context.init(keyManagerFactory.getKeyManagers(), null, null);

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .sslSocketFactory(context.getSocketFactory(), getDefaultX509TrustManager())
                .build();

        return new OkHttp3ClientHttpRequestFactory(okHttpClient);
    }

    private static X509TrustManager getDefaultX509TrustManager() throws Exception {
        TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        factory.init((KeyStore) null);
        return (X509TrustManager) factory.getTrustManagers()[0];
    }

    //todo...
}

1、创建http请求工厂

KeyStore 对象就是将我们 resources 目录下的 p12 证书加载进去,并且将 p12 证书的密码(一般为商户号)传进去。

证书这里,有一个很重要的内容,就是 SSLContext 实例化的时候用的 "TLS"

SSLContext context = SSLContext.getInstance("TLS"); 

有些教程或者资料这里用的是 "TLSv1",但是如果你启动的时候报以下的错,那就是由于 okhttp3 的版本比较高,已经不再支持 "TLSv1" 。笔者这边使用的版本是 4.9.3。

Unable to find acceptable protocols

2、实例化 RestTemplate 模板对象

RestTemplate 实例化传入的就是上面创建的 http请求工厂

    @Bean(name = "wechatRestTemplate")
    public RestTemplate restTemplate() throws Exception {
        RestTemplate restTemplate = new RestTemplate(wechatHttpRequestFactory());
        // 添加拦截器
        List interceptors = new ArrayList<>();
        RestTemplateWechatCertConfig.MyRequestInterceptor myRequestInterceptor = new RestTemplateWechatCertConfig.MyRequestInterceptor();
        interceptors.add(myRequestInterceptor);
        restTemplate.setInterceptors(interceptors);

        // 中文乱码,主要是 StringHttpMessageConverter的默认编码为ISO导致的
        List> list = restTemplate.getMessageConverters();
        for (HttpMessageConverter converter : list) {
            if (converter instanceof StringHttpMessageConverter) {
                ((StringHttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
                break;
            }
        }

        return restTemplate;
    }

注意这里 Bean 注解使用了一个别名,为的就是区分原本的 restTemplate 对象,也方便于后续实际请求微信接口的时候的注入。

二、封装微信支付相关的公共请求类

由于微信这边的接口请求使用的数据结构为 xml ,所以就另外包装了一个公共请求类。

public class WechatMiniPayBaseRequest {

    @Resource(name = "wechatRestTemplate")
    RestTemplate restTemplate;

    /**
     * post方式xml
     *
     * 返回也是xml
     *
     * @param requestUrl
     * @param reqObj
     * @return
     */
    public String postXmlData(String requestUrl, Object reqObj) {

        //请求格式 application/x-www-form-urlencoded
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        //请求参数转map
        Map params = JSONObject.parseObject(JSON.toJSONString(reqObj), HashMap.class);

        //参数加密..

        String requestXml = XmlUtil.mapToXmlStr(params);

        HttpEntity entity = new HttpEntity<>(requestXml, headers);

        String resText = restTemplate.postForObject(requestUrl, entity, String.class);

        return resText;
    }

}

可以看到我们使用的 restTemplate 注入的是上面第一步创建的针对微信支付的模板对象。

至此,我们的请求就能够携带相关证书文件了。

三、关于扩展

大家可能会发现在第一步引入 p12 证书文件的时候,是固定传入了一个证书文件路径,那么问题来了,如果一个系统内存在多个商户号,那么不同的微信小程序就需要传入对应的证书文件,可是这里是一个配置类啊,项目启动的时候就已经加载好了,怎么弄成动态的呢?而且每次去弄证书文件也麻烦啊,还要重启项目什么的,不够优雅。

笔者想过三个方案:

1)将证书路径抽象出来,由子类集成后实现传入这个路径来创建不同的 restTemplate 实例。但是明显这个方案不切合实际,如果有100个商户号,那么就需要创建100个子类。

2)能不能将 p12 证书弄成私钥公钥之类的东西带到请求里,那么就可以将它们存到数据库内,然后根据商户号动态传入这些信息项。但是可惜笔者对于这块证书加密的比较弱,没找到相关资料或者例子。

3)曲线救国的方式,其实大家可以看到,带不带证书,只是调整了 http请求工厂 的方法,那么我们就来调整这个方法,使它能动态使用不同的商户号证书。最后选择了这条路...

1、创建证书请求工厂方法

这个方法和上面的是基本一样的,不同的地方在于,我们使用一个巧妙的方法,传入商户号来拼接不同对应的证书路径。

    /**
     * 证书请求工厂
     *
     * @param mchId
     *
     * @return
     * @throws Exception
     */
    public OkHttp3ClientHttpRequestFactory wechatHttpRequestFactory(String mchId) throws Exception {

        //查看当前商户号的证书请求工厂缓存
        OkHttp3ClientHttpRequestFactory httpRequestFactory = certRequestFactoryMap.get(mchId);
        if (httpRequestFactory != null) {
            return httpRequestFactory;
        }

        KeyStore keyStore = KeyStore.getInstance("PKCS12");//eg. PKCS12

        String certPath = "wechat/" + mchId + "/apiclient_cert.p12";

        ClassPathResource classPathResource = new ClassPathResource(certPath);
        keyStore.load(classPathResource.getInputStream(), mchId.toCharArray());
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, mchId.toCharArray());

        SSLContext context = SSLContext.getInstance("TLS");
        context.init(keyManagerFactory.getKeyManagers(), null, null);

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .sslSocketFactory(context.getSocketFactory(), getDefaultX509TrustManager())
                .build();

        OkHttp3ClientHttpRequestFactory okHttp3ClientHttpRequestFactory = new OkHttp3ClientHttpRequestFactory(okHttpClient);

        //设置证书请求工厂缓存
        certRequestFactoryMap.put(mchId, okHttp3ClientHttpRequestFactory);

        return okHttp3ClientHttpRequestFactory;
    }

这里还用到一个 map 作为工厂的缓存,不同的商户号请求工厂,只创建一次,变量的实现单例。

2、请求工厂的使用

我们有了不同的工厂后,要怎么使用呢?

    @Resource
    RestTemplate restTemplate;

    public String postXmlDataWithCert(String appId, String requestUrl, Object reqObj) {

            //todo...

            OkHttp3ClientHttpRequestFactory httpRequestFactory = this.wechatHttpRequestFactory(mchId);

            restTemplate.setRequestFactory(httpRequestFactory);

            return restTemplate.postForObject(requestUrl, entity, String.class);
}

RestTemplate 我们还是用回原本框架内的,但是我们可以使用它的 setRequestFactory 方法,设置我们不同证书的请求工厂实例,这样就可以动态的使用不同的商户证书。

此乃曲线救国之道....

你可能感兴趣的:(springboot进阶应用,spring,boot,java,后端)