美文网首页
HTTPS证书校验

HTTPS证书校验

作者: 张明学 | 来源:发表于2021-01-13 10:48 被阅读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。

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

    • 将证书导入到TrustStore文件中

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

    keytool -import -file src_cer_file –keystore dest_cer_store

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

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

    • 实现自己的证书信任管理器类,比如X509TrustManager

      实现自己的证书信任管理器类就是下面要说的自定义证书校验,这种方式灵活,但需要小心。

    自定义证书校验

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

    • checkClientTrusted,该方法检查客户端的证书,无返回值,若不信任该证书则抛出异常。
    public void checkClientTrusted(X509Certificate[] arg0, String arg1){
      
    }
    
    • checkServerTrusted,该方法检查服务器的证书,无返回值,若不信任该证书同样抛出异常。
    public void checkServerTrusted(X509Certificate[] chain, String authType){
      
    }
    
    • getAcceptedIssuers,返回受信任的X509证书数组
    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);
    }
    

    相关文章

      网友评论

          本文标题:HTTPS证书校验

          本文链接:https://www.haomeiwen.com/subject/tbkuaktx.html