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

环境: 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,查找下它的实现类:
    微信支付相关-Spring RestTemplate和javax SSLContext_第1张图片
    简单检查了下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是这样:
      微信支付相关-Spring RestTemplate和javax SSLContext_第2张图片
      读微信的apiclient_cert.p12文件时是这样:
      微信支付相关-Spring RestTemplate和javax SSLContext_第3张图片
      具体值的含义和各种KeyStore的区别确实一下子搞不清、因为暂时也用不到就先放着了……
      最后通过KeyManagerFactory的init方法,将需要的商户凭证抽取出来:
      微信支付相关-Spring RestTemplate和javax SSLContext_第4张图片
      微信商户证书文件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
包依赖: 如图
微信支付相关-Spring RestTemplate和javax SSLContext_第5张图片
说明: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 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 
     *     HttpClient custom ssl example
     */
    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的话框架会自动搜索和关闭。

你可能感兴趣的:(web开发)