1. 程式人生 > >記錄spring cloud fein編寫微信支付client相關

記錄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的通訊