目录
一、创建微信支付rest模板配置类
1、创建http请求工厂
2、实例化 RestTemplate 模板对象
二、封装微信支付相关的公共请求类
三、关于扩展
1、创建证书请求工厂方法
2、请求工厂的使用
由于最近在整合微信支付相关的接口,需要带上商户证书,而官方提供的 demo 都是老掉牙的版本,使用的 Apache 的 HttpClient,而现在基本上都是用的 SpringBoot 封装的 RestTemplate,所以记录一下这些坑是怎么踩过来的。
在之前的文章 《RestTemplate集成okhttp3并自定义日志打印》已经介绍过 okhttp3 的集成,所以这篇文章重点介绍怎么使用 okhttp3 请求带 p12 证书。
至于什么是 p12 证书,怎么从微信上导出 p12 证书,不在这篇文章介绍,大家可以自行查阅资料。
由于目前框架内已经有一个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...
}
KeyStore 对象就是将我们 resources 目录下的 p12 证书加载进去,并且将 p12 证书的密码(一般为商户号)传进去。
证书这里,有一个很重要的内容,就是 SSLContext 实例化的时候用的 "TLS"
SSLContext context = SSLContext.getInstance("TLS");
有些教程或者资料这里用的是 "TLSv1",但是如果你启动的时候报以下的错,那就是由于 okhttp3 的版本比较高,已经不再支持 "TLSv1" 。笔者这边使用的版本是 4.9.3。
Unable to find acceptable protocols
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请求工厂 的方法,那么我们就来调整这个方法,使它能动态使用不同的商户号证书。最后选择了这条路...
这个方法和上面的是基本一样的,不同的地方在于,我们使用一个巧妙的方法,传入商户号来拼接不同对应的证书路径。
/**
* 证书请求工厂
*
* @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 作为工厂的缓存,不同的商户号请求工厂,只创建一次,变量的实现单例。
我们有了不同的工厂后,要怎么使用呢?
@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 方法,设置我们不同证书的请求工厂实例,这样就可以动态的使用不同的商户证书。
此乃曲线救国之道....