记录下遇到的问题和几个关键的点
背景:
cloud版本Dalston.SR5
feign client的注解用的是@RequestMapping的形式
微信那边的接口是使用XML通信的, Request和response都是XML, 接口参数需签名, 部分接口需要双向认证.
遇到的问题:
1. 如何在feign里设置双向认证证书
2. 若使用Bean发送或接收参数, 会自动使用application/json的方式处理数据
3. 使用bean作为发送参数后签名失败
问题解决:
1. 如何在feign里设置双向认证证书?
自定义feign的configuration, 然后@Bean覆盖Client. 在新的构造里添加双向认证.
@Bean
public Client feignClient() {
return new Client.Default(
TrustingSSLSocketFactory.get("MMPayCert"),
new NoopHostnameVerifier());
}
TrustingSSLSocketFactory类参考自
https://github.com/OpenFeign/feign/blob/master/core/src/test/java/feign/client/TrustingSSLSocketFactory.java
因为一开始不知道微信支付的证书key是MMPayCert, 对SSLContext的构造部分代码做了改动, 使构造时载入加载p12文件的KeyStore类, 并在SSLContext成功构建后打断点查看内部属性, 找到对应的key. 改动后的类初始化代码:
private TrustingSSLSocketFactory(String serverAlias) {
try {
KeyStore keyStore =
loadKeyStore(new ClassPathResource("/cert/apiclient_cert.p12").getInputStream());
SSLContext sslcontext = SSLContexts.custom()
.loadKeyMaterial(keyStore, KEYSTORE_PASSWORD)
.build();
this.delegate = sslcontext.getSocketFactory();
this.serverAlias = serverAlias;
if (serverAlias.isEmpty()) {
this.privateKey = null;
this.certificateChain = null;
} else {
this.privateKey = (PrivateKey) keyStore.getKey(serverAlias, KEYSTORE_PASSWORD);
Certificate[] rawChain = keyStore.getCertificateChain(serverAlias);
this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
问题1解决. 这时我写的client method还是纯String通信的, 觉得需要改进, 就写了bean来接收响应, 请求体暂不改, 还是String, 因为涉及签名比较麻烦.
由此遇到了问题2
2. 若使用Bean发送或接收参数, 会自动使用application/json的方式处理数据
查日志, 微信的接口返回数据跟accept头完全对不上, 编写自定义的docker解决, 虽说是自定义, 但其实只需要选取现成的合适的convert类再继承后重设下可支持的MediaType就好了.
public class WxPayResponseConverter extends MappingJackson2XmlHttpMessageConverter {
public WxPayResponseConverter() {
List mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.TEXT_HTML);
mediaTypes.add(MediaType.TEXT_XML);
setSupportedMediaTypes(mediaTypes);
}
}
使用方法, 同样写入feign配置类里
@Bean
public Decoder decoder() {
HttpMessageConverter> additional = new WxPayResponseConverter();
ObjectFactory objectFactory = () -> new HttpMessageConverters(additional);
return new ResponseEntityDecoder(new SpringDecoder(objectFactory));
}
此时数据的接收已可直接从XML注入bean.
然后再是把请求数据结构也改成bean.
同样添加自定义encoder, 与上面几乎一样, 换个返回类型和方法名就行. 不再贴代码.
改完后测试, 发现返回提示XML解析错误, 看日志发现过去的还是JSON数据, 且请求头为application/json
在@RequestMapping里添加headers={"content-type=text/xml"}再试. 结果依然不行, 可以看到日志里请求头已经是text/xml 但数据还是json格式.
debug SpringEncoder的encode方法, 里面有重要的一句
Collection contentTypes = (Collection)request.headers().get("Content-Type");
请求头必须是Content-Type............................................................WTF
修改后
@RequestMapping(value = WX_PAY_API_B2C, method = RequestMethod.POST, headers = {"Content-Type=text/xml"})
然后再试, 出现问题3. 提示签名失败.
3 使用bean作为发送参数后签名失败
签名的方法原本使用的是一个给Map
然后微信支付里的参数有许多带下划线, 非驼峰格式, 且我的bean加了驼峰处理, 使用Jackon注解定义转换的参数名.
问题就出在这里!!!
上述bean2map的转换使用反射完成, 没做判断, 直接使用了bean里field的name来做map的key, 导致签名参数名与传递的不符.
解决, 增加判断逻辑. 保证签名正常, 只贴bean2map的部分, 其余的签名代码就不贴了.
Map data = new HashMap<>();
Field[] declaredFields = object.getClass().getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
String fieldName = field.isAnnotationPresent(JsonProperty.class) ?
field.getAnnotation(JsonProperty.class).value() : field.getName();
Object value = field.get(object);
if (value == null)
continue;
data.put(fieldName, value.toString());
}
至此实现feign客户端与微信支付API的通信