记录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 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使用稍作了改动, 时签名之前完成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的通信

你可能感兴趣的:(java)