HTTPS证书校验

2021-01-13  本文已影响0人  张明学

HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在HTTP 的基础下加入SSL,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间)。这个系统提供了身份验证与加密通讯方法。它被广泛用于万维网上安全敏感的通讯,例如交易支付等方面。

关于HTTPS证书的知识,水还是很深的本文先介绍一下我们常用的问题。

证书校验

默认的证书校验

在Java中要访问Https链接时,会用到一个关键类HttpsURLConnection;参见如下实现代码:

URL serverUrl = new URL("https://www.xxx.com");
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) serverUrl.openConnection();
int responseCode = httpsURLConnection.getResponseCode();

在取得httpsURLConnection的时候和正常浏览器访问一样,仍然会验证服务端的证书是否被信任(权威机构发行或者被权威机构签名);如果服务端证书不被信任,则默认的实现就会有问题。当证书过期时,默认的证书校验也。一般来说,用SunJSSE会抛如下异常信息:

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)

SunJSSE(Sun的Java安全套接扩展 Java Secure Socket Extension)是sun公司实现Internet安全通信的一系列包的集合。它是一个SSL和TLS的纯Java实现,可以透明地提供数据加密、服务器认证、信息完整性等功能,JSSE是一个开放的标准,不只是Sun公司才能实现一个SunJSSE,事实上其他公司有自己实现的JSSE,然后通过JCA就可以在JVM中使用(暂时还没研究过,一般都是用sun实现的)。

在深入了解JSSE之前,需要了解一个有关Java安全的概念:客户端的TrustStore文件。客户端的TrustStore文件中保存着被客户端所信任的服务器的证书信息。客户端在进行SSL连接时,JSSE将根据这个文件中的证书决定是否信任服务器端的证书。

JSSE中,有一个信任管理器类负责决定是否信任远端的证书,这个类有如下的处理规则:

⑴ 果系统属性javax.net.sll.trustStore指定了TrustStore文件,那么信任管理器就去jre安装路径下的lib/security/目录中寻找并使用这个文件来检查证书。

⑵ 果该系统属性没有指定TrustStore文件,它就会去jre安装路径下寻找默认的TrustStore文件,这个文件的相对路径为:lib/security/jssecacerts。

⑶ 如果 jssecacerts不存在,但是cacerts存在(它随J2SDK一起发行,含有数量有限的可信任的基本证书),那么这个默认的TrustStore文件就是cacerts。

当那遇到上面证书校验不通过的这种情况,怎么处理呢?有以下两种方案:

Java提供了命令行工具keytool用于创建证书或者把证书从其它文件中导入到Java自己的TrustStore文件中。把证书从其它文件导入到TrustStore文件中的命令行格式为:

keytool -import -file src_cer_file –keystore dest_cer_store

其中,src_cer_file为存有证书信息的源文件名,dest_cer_store为目标TrustStore文件。

这种方式不灵活,对于移动应用来说基本无效,不可能让每一位用户手动安装一下证书。

自定义证书校验

自定义证书的校验一般是实现X509TrustManager接口,该接口有三个方法需要实现。

public void checkClientTrusted(X509Certificate[] arg0, String arg1){
  
}
public void checkServerTrusted(X509Certificate[] chain, String authType){
  
}
public X509Certificate[] getAcceptedIssuers() {
  
}
自定义部分规则校验
/**
 * 自定义部分规则校验,主要是针对服务器返回证书信息进行校验
 */
public class TrustCerManager implements X509TrustManager {

    private Certificate[] mCertificates;
    private String[] localPublicKeyStrs;
        /**
         * 构造方法,传入本地信任的证书cer文件所获取的Certificate信息。根据Certificate获取公钥信息
         */
    public TrustCerManager(Certificate[] argCers) {
        mCertificates = argCers;
        localPublicKeyStrs = new String[argCers.length];
        for (int i = 0; i < mCertificates.length; i++) {
            Certificate cer = mCertificates[i];
            PublicKey publicKey = cer.getPublicKey();
            // 将公钥信息解析出来
            localPublicKeyStrs[i] = byte2Base64(publicKey.getEncoded());
        }
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        if (null != localPublicKeyStrs) {
            int passCount = 0;
            for (String publicKeyStr : localPublicKeyStrs) {
                if (null != chain) {
                    // 遍历服务端证书链的证书信息
                    for (X509Certificate c : chain) {
                        // 获取服务端证书链的证书公钥
                        PublicKey netPublicKey = c.getPublicKey();
                        String netPublicKeyStr = byte2Base64(netPublicKey.getEncoded());
                                                // 记录是否存在服务端证书链上的证书公钥与本地信任的相同
                        if (publicKeyStr.equals(netPublicKeyStr)) {
                            passCount++;
                        }
                        log.info("PublicKey={}", netPublicKey.toString());
                    }
                }
            }

            log.info("passCount={}", passCount);

            if (0 == passCount) {
                // 没有一个比配上的证书公钥,抛出异常,若不信任该证书
                throw new CertificateException();
            }

        } else {
            System.out.println("CertificateException");
            throw new CertificateException();
        }

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }


    /**
     * 字节数组转Base64编码
     *
     * @param bytes
     * @return
     */
    public static String byte2Base64(byte[] bytes) {
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(bytes);
    }

    /**
     * Base64编码转字节数组
     *
     * @param base64Key
     * @return
     * @throws IOException
     */
    public static byte[] base642Byte(String base64Key) throws IOException {
        BASE64Decoder decoder = new BASE64Decoder();
        return decoder.decodeBuffer(base64Key);
    }

}

根据cer文件获取Certificate

CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate cert = cf.generateCertificate(new FileInputStream("my.cer"));

这种方式可以解决证书过期问题,尤其移动端APP端的校验,当某一天证书过期了,可能保证客户端还能访问,也提高了证书的校验速度。

自定义完全信任证书

完全信任证书是指信任证书所有服务端的证书,无论它是否过期,是否经过认证。一般这种情况用在信任的域上面。

public class TrustAllManager implements X509TrustManager {
    /**
     * 该方法检查客户端的证书,若不信任该证书则抛出异常。由于我们不需要对客户端进行认证,因此我们只需要执行默认的信任管理器的这个方法。JSSE中,默认的信任管理器类为TrustManager。
     *
     * @param arg0
     * @param arg1
     * @throws CertificateException
     */
    @Override
    public void checkClientTrusted(X509Certificate[] arg0, String arg1)
            throws CertificateException {

    }

    /**
     * 该方法检查服务器的证书,若不信任该证书同样抛出异常。通过自己实现该方法,可以使之信任我们指定的任何证书。在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。
     *
     * @param chain
     * @param authType
     * @throws CertificateException
     */
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
    }

    /**
     * 返回受信任的X509证书数组
     *
     * @return
     */
    @Override
    public X509Certificate[] getAcceptedIssuers() {
        X509Certificate[] x509Certificates = new X509Certificate[0];
        return x509Certificates;
    }

}
X509TrustManager的使用

通常我们使用http请求除了HttpURLConnection外,用的第三方库有:OkHttp和HttpClient。下面分别介绍一下这三种式设置TrustManager的方法。

/**
 * 根据自定义的X509TrustManager构建一个SSLSocketFactory
 */
public static SSLSocketFactory createAllSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
    TrustManager[] trustManagers = new TrustManager[]{new TrustAllManager()};
    SSLContext context = SSLContext.getInstance("SSL");
    context.init(null, trustManagers, new SecureRandom());
    return context.getSocketFactory();
}

createAllSSLSocketFactory方法下面三种方式都是使用到。

HttpURLConnection
URL serverUrl = new URL(url);
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) serverUrl.openConnection();
// 设置SSLSocketFactory
httpsURLConnection.setSSLSocketFactory(createAllSSLSocketFactory());

int responseCode = httpsURLConnection.getResponseCode();
OkHttp
OkHttpClient.Builder builder = new OkHttpClient.Builder();
// 设置hostnameVerifier
builder.hostnameVerifier((s, sslSession) -> true);
// 设置SSLSocketFactory
builder.sslSocketFactory(createAllSSLSocketFactory(), new TrustAllManager());

Request request = new Request.Builder()
        .url(url)
        .build();
Response response = builder.build().newCall(request).execute();
HttpClient
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(createAllSSLSocketFactory(), NoopHostnameVerifier.INSTANCE);

CloseableHttpClient client = HttpClients.custom()
            // 设置SSLSocketFactory
        .setSSLSocketFactory(sslsf)
        .build();
//发送get请求
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);

/**请求发送成功,并得到响应**/
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
    /**读取服务器返回过来的json字符串数据**/
    String strResult = EntityUtils.toString(response.getEntity());
    System.out.println(strResult);
}
上一篇下一篇

猜你喜欢

热点阅读