記錄spring cloud fein編寫微信支付client相關
記錄下遇到的問題和幾個關鍵的點
背景:
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<MediaType> 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<HttpMessageConverters> 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<String> 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<String, String> 簽名的靜態工具類裡的方法, 為了能給bean使用稍作了改動, 時簽名之前完成bean -> Map<String, String>的轉換
然後微信支付裡的引數有許多帶下劃線, 非駝峰格式, 且我的bean加了駝峰處理, 使用Jackon註解定義轉換的引數名.
問題就出在這裡!!!
上述bean2map的轉換使用反射完成, 沒做判斷, 直接使用了bean裡field的name來做map的key, 導致簽名引數名與傳遞的不符.
解決, 增加判斷邏輯. 保證簽名正常, 只貼bean2map的部分, 其餘的簽名程式碼就不貼了.
Map<String, String> 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的通訊