微信支付相关-Spring RestTemplate和javax SSLContext

来源:互联网 发布:热血屠龙灵兽进阶数据 编辑:程序博客网 时间:2024/06/05 16:59

环境: java8+Spring

起因: 4月写微信支付的后台中间接口时,申请退款请求需要带上商户证书。微信官方给的java demo用的是apache的HttpClient,但因为实际server用的是Spring…所以就考虑怎么在Spring的RestTemplate里引入商户凭证,结果发现牵扯出来很多东西…这里整理了一些。

思路:

  1. RestTemplate部分
    Spring用的不久,稍微仔细点地看了下代码、才发现RestTemplate诚如其名就是封装好的模板。它众多的xxForObject、xxForEntity方法,内部流程其实很简明:

    /* RestTemplate#doExecute */// ClientHttpRequestFactory#createRequest,生成一个具体请求实例ClientHttpRequest request = createRequest(url, method);if (requestCallback != null) {    // 请求headers和body处理,用到了HttpMessageConverter    requestCallback.doWithRequest(request); }//ClientHttpRequest#execute 执行请求response = request.execute(); // response error处理handleResponse(url, method, response);

    于是只看ClientHttpRequestFactory,查找下它的实现类:
    ClientHttpRequestFactory Hierarchy
    简单检查了下import、只有基于HttpComponents、Netty和OkHttp的涉及到了ssl。几个request factory都可以通过constuctor注入相应client实例,所以就回到了HttpComponents/Netty/OkHttp怎么加ssl……然后矛头都指向了一个class:SSLContext

  2. 证书相关
    因为之前读书少在看微信demo时发现通篇的双向认证、公钥私钥、KeyStore、PKCS12……几乎都是陌生词汇。网络安全相关到目前才了解皮毛……还好只是写个退款接口、搞起几个相关的概念就写完了:

    1. 首先必须先了解https签名认证流程。SO上看到推荐Wikipedia Public-key cryptography里面的Postal analogies(邮局比喻)、确实写的好。其他就自行百度google了。这里记录的只有一个点就是证书和公钥的区别:按Wikipedia的Public key certificate第一句就是“数字证书是用于证明公钥所有者身份的电子文档”、按SE的这个问题“X.509证书至少包含1)公钥和2)公钥所有者信息”,一开始没搞清还是很影响理解的……

    2. 然后直接回来看SSLContext,简单搜下例程知道它是先getInstance然后init, getInstance没有深入看,总之先按demo用TLSv1协议;init参数有3个,均可为null、null时用系统默认:

      public final void init(KeyManager[] var1, TrustManager[] var2, SecureRandom var3) throws KeyManagementException       

      KeyManager 管理本地使用者的密钥证书信息,微信商户凭证就是由此引入;
      TrustManager 管理受信任的服务方的信息。这些信息可以每次向权威证书认证机构查询、也可以自己看准了直接导入(比如12306让干的)。微信demo关于rootca.pem的说明文档提到:“某些环境和工具已经内置了若干权威机构的根证书,无需引用该证书也可以正常进行验证,这里提供给您在未内置所必须根证书的环境中载入使用“,微信或腾讯必然在某家CA认证过,Java的话有默认的CA列表(cacerts文件,jdk目录下,用.\bin\keytool.exe -list -keystore .\jre\lib\security\cacerts查看)。所以这个TrustManager可以直接设成null用系统默认的(就是demo里写的那样)。
      SecureRandom 应该是https双向认证成功后、对称加密时用的随机数生成器,这个也可以用默认不用考虑。

    3. 微信给的商户证书是文件,要读取成程序运行时的数据、所以就用到了FileInputStream和KeyStore。
      看javadoc,KeyStore和KeyStoreSpi作用是统一封装存放了不同算法的证书密钥,
      比如读前面的cacerts是这样:
      keystore-jks
      读微信的apiclient_cert.p12文件时是这样:
      keystore-wx
      具体值的含义和各种KeyStore的区别确实一下子搞不清、因为暂时也用不到就先放着了……
      最后通过KeyManagerFactory的init方法,将需要的商户凭证抽取出来:
      KeyManagerFactory
      微信商户证书文件apiclient_cert.p12的格式为PKCS12,按(还是)Wikipedia说的”It is commonly used to bundle a private key with its X.509 certificate”,上图debug信息、和demo给的“导出的apiclient_cert.pem(证书文件)和apiclient_key.pem(私钥文件)”验证了这一说法。不过demo文档还提到“服务器验证客户端的时候通过客户端证书和签名(既:apiclient_cert.p12 或者 apiclient_cert.pem和apiclient_key.pem)”,并不是很确定为什么这样说。


综上,一个非常简单的test main:

语言: java
包依赖: 如图
包依赖
说明:3个RestTemplate的factory配置,调试学习用
代码:

import io.netty.handler.ssl.SslContext;import io.netty.handler.ssl.SslContextBuilder;import okhttp3.OkHttpClient;import org.apache.http.conn.ssl.SSLConnectionSocketFactory;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClients;import org.apache.http.ssl.SSLContexts;import org.springframework.beans.factory.DisposableBean;import org.springframework.http.ResponseEntity;import org.springframework.http.client.ClientHttpRequestFactory;import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;import org.springframework.http.client.Netty4ClientHttpRequestFactory;import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;import org.springframework.http.converter.StringHttpMessageConverter;import org.springframework.web.client.RestTemplate;import javax.net.ssl.*;import java.io.FileInputStream;import java.nio.charset.StandardCharsets;import java.security.KeyStore;import java.security.SecureRandom;public class RestTemplateExample {    private static final String REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund";    public static void main(String[] args) throws Exception {        final String certFile = args[0]; // cert file location        final String passwd = args[1]; // mch_id        KeyStore keyStore = loadFrom("PKCS12", certFile, passwd);        // httpComponent        ClientHttpRequestFactory factory = createHttpComponentFactory(keyStore, passwd);        testGet(factory);        // okhttp        factory = createOkHttp3Factory(keyStore, passwd);        testGet(factory);        // netty        factory = createNettyFactory(keyStore, passwd);        testGet(factory);        ((DisposableBean) factory).destroy();        System.out.println("end");    }    private static void testGet(ClientHttpRequestFactory factory) {        RestTemplate restTemplate = new RestTemplate(factory);        System.out.println("using " + restTemplate.getRequestFactory().getClass());        restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));        ResponseEntity<String> getRes = restTemplate.getForEntity(REFUND_URL, String.class);        System.out.println(getRes.getBody());    }    private static KeyStore loadFrom(String type, String fileName, String passwd) throws Exception {        KeyStore keyStore = KeyStore.getInstance(type);        try (FileInputStream fileIn = new FileInputStream(fileName)) {            keyStore.load(fileIn, passwd.toCharArray());        }        System.out.println("keystore entries: " + keyStore.size());        return keyStore;    }    private static ClientHttpRequestFactory createOkHttp3Factory(KeyStore keyStore, String passwd) throws Exception {        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());        keyManagerFactory.init(keyStore, passwd.toCharArray());        SSLContext context = SSLContext.getInstance("TLSV1");        context.init(keyManagerFactory.getKeyManagers(), null, null);        OkHttpClient okHttpClient = new OkHttpClient.Builder()            .sslSocketFactory(context.getSocketFactory(), getDefaultX509TrustManager())            .build();        return new OkHttp3ClientHttpRequestFactory(okHttpClient);    }    /**     * @see OkHttpClient.Builder#sslSocketFactory(SSLSocketFactory)     * @see OkHttpClient.Builder#sslSocketFactory(SSLSocketFactory, X509TrustManager)     * @see sun.security.ssl.SSLContextImpl#engineInit(KeyManager[], TrustManager[], SecureRandom)     */    private static X509TrustManager getDefaultX509TrustManager() throws Exception {        TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());        factory.init((KeyStore) null);        return (X509TrustManager) factory.getTrustManagers()[0];    }    /**     * @see <a href="https://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientCustomSSL.java">     *     HttpClient custom ssl example</a>     */    private static ClientHttpRequestFactory createHttpComponentFactory(KeyStore keyStore, String passwd) throws Exception {        SSLContext sslcontext = SSLContexts.custom()            .loadKeyMaterial(keyStore, passwd.toCharArray()).build();        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(            sslcontext, new String[]{"TLSv1"}, null,            SSLConnectionSocketFactory.getDefaultHostnameVerifier());        CloseableHttpClient httpclient = HttpClients.custom()            .setSSLSocketFactory(sslsf).build();        return new HttpComponentsClientHttpRequestFactory(httpclient);    }    private static ClientHttpRequestFactory createNettyFactory(KeyStore keyStore, String passwd) throws Exception {        SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());        keyManagerFactory.init(keyStore, passwd.toCharArray());        SslContext sslContext = sslContextBuilder.keyManager(keyManagerFactory).build();        Netty4ClientHttpRequestFactory factory = new Netty4ClientHttpRequestFactory();        factory.setSslContext(sslContext);        return factory;    }}

上面写不下的注释:
1. OkHttp部分,按它javadoc的意思,它需要一个X509TrustManager来处理cert chain,虽然SSLSocketFactory的实现类里就包着一个、但因为没有public get方法、要拿只能靠反射;为了不用反射使源码变得难看,就只好请开发者在client端调用时传一个进来;即使这样也还是很难看、且自行导入的CA列表也可能不安全、所以javadoc里也不建议这么做……
2. HttpClient包下不少ssl相关的class都被deprecate了,参考的apache官方示例(就是微信例程用法…)稍微改了下。
3. Netty factory会按url重用BootStrap和新建Channel连接,但EventLoopGroup线程池可以通用;实际用Spring的话框架会自动搜索和关闭。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 孩子毎天通宵游戏怎么办 熬夜写作业困了怎么办 三十多岁白头发越来越多怎么办 AI界面字体太小怎么办 睡不着怎么办躺倒床上脑子混乱 作息规律不正常夜里睡不着怎么办 作息不规律引起身体痒怎么办 在大学里好无聊怎么办 开会时间通知错了怎么办 商场要求商户变更位置怎么办 怀孕初期症状小腹痛怎么办 1岁半宝宝吃夜奶怎么办 戒奶宝宝不喝奶粉怎么办 2岁半宝宝老晚睡怎么办 老是熬夜然后想调生物钟怎么办 一个月宝宝睡眠不好怎么办 个人怎么办一清pos机 社保到退休年龄未交满15年怎么办 退休时社保没交满15时怎么办 单位不支付病假工资怎么办 一年级学生上课注意力不集中怎么办 一年级学生的理解能力差怎么办 一年级学生学习太差怎么办 宝宝屁眼破皮怎么办啊 九个月婴儿不爱喝奶怎么办 十一个月婴儿发烧怎么办 四个月宝宝睡不踏实怎么办 5个月宝宝瘦了怎么办 宝宝只吃迷糊奶怎么办 宝宝五个月了不吃奶粉怎么办 1岁婴儿入睡困难怎么办 怀孕五个月胎儿肾积水怎么办 15个月宝宝总喊怎么办 学业水平考试有d怎么办 买了水果碰见领导怎么办 高一孩子不愿意上学怎么办 专家解答 孩子不愿意上学怎么办 冬天脚冷怎么办膝盖疼 拉拉裤大了怎么办小妙招 孕37周翻身困难怎么办 晚上睡不好白天犯困怎么办