【HTTPS】1、使用jdk实现https接口调用和证书验证

概述

我们之前调用https都是不做证书验证的,因为我们实现X509TrustManager方法的时候并没有具体实现里面的方法,而是不实现,那么这就会导致一个问题,那就是证书有正确性是没有得到有效验证的
常规的方法我们如果想验证的话,那就是不实现X509TrustManager,用jdk自带的方法进行接口调用,但是这个要求我们用keytools对第三方的证书进行导入,并且会对jdk自带的信任库有侵入性修改,这个软件每次安全都得重新导入对应的证书
那么有没有版本不需要手动导入操作,也能进行对端证书验证呢?
因为客户端证书是可以再浏览器上直接下载的,那么如果我们能用代码实现证书的下载,并且安装到本地,不就可以实现证书的自动安装了么?(当前这样做也不安全,因为并不是所有的证书都需要安装,但是我们可以加一个开关或者一个业务上配置,判断那些网站是可以信任,那些不需要信任,然后对信任的网站进行自动证书安装)

当前这样做也不安全,因为并不是所有的证书都需要安装,但是我们可以加一个开关或者一个业务上配置,判断那些网站是可以信任,那些不需要信任,然后对信任的网站进行自动证书安装
当然如果是互联网这个做法就有点脱裤子放屁的嫌疑,但是如果这个是局域网/内网的情况下就很有效果了

代码实现

用来保存证书的代码

package https.util;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.X509TrustManager;

/**
 * 功能描述
 *
 * @since 2022-05-09
 */
public class SaveCertManager implements X509TrustManager {

    private final X509TrustManager tm;
    public X509Certificate[] chain;

    public SaveCertManager(X509TrustManager tm) {
        this.tm = tm;
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        this.chain = chain;
        tm.checkClientTrusted(chain, authType);
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        this.chain = chain;
        tm.checkServerTrusted(chain, authType);
    }

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

用来校验证书的代码

package https.util;

import java.security.cert.CertificateException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.X509TrustManager;

/**
 * 功能描述
 *
 * @since 2022-05-09
 */
public class CheckCertManager  implements X509TrustManager {

    private final X509TrustManager tm;
    public X509Certificate[] chain;
    // 吊销列表
    private X509CRL[] x509CRLs;

    public CheckCertManager(X509TrustManager tm) {
        this.tm = tm;
    }

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

        // 吊销证书验证
        if (x509CRLs != null) {
            Arrays.stream(chain).forEach(cert -> {
                Arrays.stream(x509CRLs).forEach(x509CRL -> {
                    if (x509CRL.isRevoked(cert)) {
                        // 证书已经吊销
                        System.out.println("证书已经吊销");
                    }
                });
            });
        }
    }

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

        // 吊销证书验证
        if (x509CRLs != null) {
            Arrays.stream(chain).forEach(cert -> {
                Arrays.stream(x509CRLs).forEach(x509CRL -> {
                    if (x509CRL.isRevoked(cert)) {
                        // 证书已经吊销
                        System.out.println("证书已经吊销");
                    }
                });
            });
        }
    }

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

    public X509CRL[] getX509CRLs() {
        return x509CRLs;
    }

    public void setX509CRLs(X509CRL[] x509CRLs) {
        this.x509CRLs = x509CRLs;
    }
}

package https.util;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.text.Normalizer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;

import sun.security.x509.CRLDistributionPointsExtension;
import sun.security.x509.URIName;
import sun.security.x509.X509CertImpl;

/**
 * 功能描述
 *
 * @since 2022-05-09
 */
public class HttpsUtil2022 {

    /**
     * 默认jdk的证书仓库名称
     */
    private static String CERTS_SOURCE_FILE = "jssecacerts";

    /**
     * 默认的ca证书文件名称
     */
    private static String CERTS_SOURCE_CA_FILE = "cacerts";

    public static String sendPostWithCheckCertAndRevokedCheck(Map params) throws UnsupportedEncodingException {
        ByteArrayOutputStream ans = new ByteArrayOutputStream();
        // https请求,自动加载证书,并进行证书有效期验证,并且自动加载crl证书分发点,然后进行证书吊销验证
        // 1. 判断是否需要进行证书下载
        // 2. 下载crl证书吊销分发点
        boolean checkres = checkAndInstallCertAndCrl(params);
        // 3. 创建https连接,并且使用指定的证书进行有效期验证,并且使用指定的证书校验是否吊销
        // 1.读取jks证书文件
        String templateHome = MapUtils.getString(params, "templateHome");
        String filePath = templateHome + File.separator + MapUtils.getString(params, "fileName");
        String crlFilePath = templateHome + File.separator + "crl";
        String sourcePasswd = MapUtils.getString(params, "sourcePasswd");
        String url = MapUtils.getString(params, "url");
        String host = MapUtils.getString(params, "host");
        String request = MapUtils.getString(params, "request");

        File certsFile = new File(filePath);
        if (!certsFile.isFile()) {
            filePath = replaceCRLF(System.getProperty("java.home") + File.separator + "lib" + File.separator + "security");
            filePath = Normalizer.normalize(filePath, Normalizer.Form.NFKC);
            File dir = new File(filePath);
            certsFile = new File(dir, CERTS_SOURCE_FILE);
            if (!certsFile.isFile()) {
                certsFile = new File(dir, CERTS_SOURCE_CA_FILE);
            }
            filePath = certsFile.getPath();
            sourcePasswd = MapUtils.getString(params, "jdkcertPasswd");
        }

        try (InputStream inputStream = new FileInputStream(filePath)) {
            URL url1 = new URL(url);
            HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url1.openConnection();

            if (checkres) {
                // 这里需要从新加载,不然读取的是老的content
                // 3.读取证书,初始化ssl
                // 默认是jks的证书
                KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                // 加载
                keyStore.load(inputStream, sourcePasswd.toCharArray());
                SSLContext sslContext = SSLContext.getInstance("TLSv1.2", "SunJSSE");
                TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                trustManagerFactory.init(keyStore);
                X509TrustManager x509TrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
                CheckCertManager checkCertManager = new CheckCertManager(x509TrustManager);

                // 读取吊销列表
                File crldir = new File(crlFilePath);
                if (crldir.exists() && crldir.isDirectory() && crldir.listFiles() != null) {
                    File[] crlFiles = crldir.listFiles();
                    X509CRL[] x509CRLs = new X509CRL[crlFiles.length];
                    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                    InputStream crlInputStream = null;
                    for (int i = 0; i < crlFiles.length; ++i) {
                        crlInputStream = new FileInputStream(crlFiles[i]);
                        X509CRL crl = (X509CRL) certificateFactory.generateCRL(crlInputStream);
                        crlInputStream.close();
                        x509CRLs[i] = crl;
                    }
                    checkCertManager.setX509CRLs(x509CRLs);
                }
                sslContext.init(null, new TrustManager[]{checkCertManager}, SecureRandom.getInstanceStrong());
                String finalHost = host;
                httpsURLConnection.setHostnameVerifier((str, sslSession) -> sslVerify(str, "true", finalHost));
                httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
            }
            // 4.发送https请求,基于当前已经安装的证书
            // 设置请求头
//            httpsURLConnection.setRequestProperty("Content-Type", "application/json");

            // 设置请求头
            Map<String, String> heads = (Map) params.get("heads");
            if (heads == null) {
                heads = new HashMap<>();
            }
            for (Map.Entry<String, String> entry : heads.entrySet()) {
                httpsURLConnection.addRequestProperty(entry.getKey(), entry.getValue());
            }

            // 设置请求方法 requestMethod
            httpsURLConnection.setRequestMethod(MapUtils.getString(params, "requestMethod", "GET"));
//            httpsURLConnection.setDoOutput(true);
            httpsURLConnection.connect(); // 建立实际连接

            // 发送请求数据
            InputStream urlInputStream;
            if (StringUtils.isNotBlank(request)) {
                OutputStream urlOutputStream = httpsURLConnection.getOutputStream();
                urlOutputStream.write(request.getBytes());
                urlOutputStream.flush();
            }

            // 获取返回结果
            int index;
            byte[] bytes = new byte[2048];
            if (httpsURLConnection.getResponseCode() == 200) {
                // 请求成功输出数据
                urlInputStream = httpsURLConnection.getInputStream();
                // 读取数据

            } else {
                // 输出错误信息
                urlInputStream = httpsURLConnection.getErrorStream();
            }
            // 返回数据
            while ((index = urlInputStream.read(bytes)) != -1) {
                // 写出数据集
                ans.write(bytes, 0, index);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return ans.toString();
    }

    private static boolean sslVerify(String str, String isCheck, String hostname) {
        if ("false".equals(isCheck)) {
            return true;
        }
        if (StringUtils.isNotEmpty(hostname)) {
            if (check(str, hostname)) {
                return true;
            }
        }
        return false;
    }

    private static boolean check(String str, String hostname) {
        if (hostname.contains(",")) {
            String[] hostnames = hostname.split(",");
            if (checkForeach(str, hostnames)) {
                return true;
            }
        } else {
            if (str.equalsIgnoreCase(hostname)) {
                return true;
            }
        }
        return false;
    }

    private static boolean checkForeach(String str, String[] hostnames) {
        for (String host : hostnames) {
            if (str.equalsIgnoreCase(host)) {
                return true;
            }
        }
        return false;
    }

    private static boolean checkAndInstallCertAndCrl(Map params) {
        boolean ans = true;
        // 功能: 判断是否需要进行证书下载 ,下载crl证书吊销分发点
        // 1. 获取证书路径
        // 1. 存放证书认证信息路径
        String templateHome = MapUtils.getString(params, "templateHome");
        String filePath = templateHome + File.separator + MapUtils.getString(params, "fileName");
        File certsFile = new File(filePath);
        String sourcePasswd = MapUtils.getString(params, "sourcePasswd");
        String protocol = MapUtils.getString(params, "sslVersion");
//        host, port
        String host = MapUtils.getString(params, "host");
        int port = MapUtils.getInteger(params, "port");

        if (!certsFile.isFile()) {
            filePath = replaceCRLF(System.getProperty("java.home") + File.separator + "lib" + File.separator + "security");
            filePath = Normalizer.normalize(filePath, Normalizer.Form.NFKC);
            File dir = new File(filePath);
            certsFile = new File(dir, CERTS_SOURCE_FILE);
            if (!certsFile.isFile()) {
                certsFile = new File(dir, CERTS_SOURCE_CA_FILE);
            }
            sourcePasswd = MapUtils.getString(params, "jdkcertPasswd");
            ans = false;
        }
        // 2. 当前证书库中是否已经对这个网站信任了 --
        OutputStream outputStream = null;
        try (InputStream inputStream = new FileInputStream(certsFile)) {
            // 3. 尝试一次连接,并获取证书对象
            // 默认是jks的证书
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            // 加载
            keyStore.load(inputStream, sourcePasswd.toCharArray());
            // 2.构建TLS请求
            SSLContext sslContext = SSLContext.getInstance(protocol, "SunJSSE");
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);
            // 读取默认的管理器
            X509TrustManager x509TrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
            SaveCertManager trustCertsSaveManager = new SaveCertManager(x509TrustManager);
            sslContext.init(null, new TrustManager[]{trustCertsSaveManager}, SecureRandom.getInstanceStrong());
            SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
            // 4.-1 判断是否需要安装
            // 3.判断是否需要安装
            // 4.读取证书链,依次安装输入到文件中
            if (!needInstall(sslSocketFactory, host, port)) {
                return ans;
            }
            // 读取需要安装的证书链
            X509Certificate[] x509Certificates = trustCertsSaveManager.chain;
            if (x509Certificates == null) {
                return false;
            }

            // 4. 获取证书的吊销列表
            outputStream = new FileOutputStream(filePath);
            for (int i = 0; i < x509Certificates.length; i++) {
                X509CertImpl x509Cert = (X509CertImpl) x509Certificates[i];
                // 获取crl分发点,然后下载证书文件
                CRLDistributionPointsExtension crlDistributionPointsExtension = x509Cert.getCRLDistributionPointsExtension();
                // 下载所有
                // 5. 下载保存证书的吊销列表
                downloadCrlAndCreateDir(crlDistributionPointsExtension, templateHome);

                // 6. 输出保存证书对象
                String alias = host + "-" + i + 1;
                X509Certificate x509Certificate = x509Certificates[i];
                // 加载证书
                keyStore.setCertificateEntry(alias, x509Certificate);
                // 输出到证书文件
                keyStore.store(outputStream, MapUtils.getString(params, "sourcePasswd").toCharArray());
            }
            outputStream.flush();
            ans = true;
        } catch (Exception e) {
            ans = false;
            e.printStackTrace();
        } finally {
            closeStream(outputStream, null);
        }
        return ans;
    }

    private static void closeStream(OutputStream outputStream, InputStream inputStream) {
        // 关闭流
        if (outputStream != null) {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
//                LOGGER.error("Exception:" + e);
            }
            outputStream = null;
        }

        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
//                LOGGER.error("Exception:" + e);
                e.printStackTrace();
            }
            inputStream = null;
        }
    }

    private static void downloadCrlAndCreateDir(CRLDistributionPointsExtension crlDistributionPointsExtension, String basePath) throws IOException {
        if (crlDistributionPointsExtension == null) {
            return;
        }
        // 下载
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        Date date = new Date();
        // 获取吊销地址
        crlDistributionPointsExtension.get(CRLDistributionPointsExtension.POINTS).forEach(distributionPoint -> {
            // 循环每一个吊销列表
            int[] index = {0};
            distributionPoint.getFullName().names().forEach(generalName -> {
                URIName uri = (URIName) generalName.getName();
                // 下载uri
                try {
                    downloadCrl(uri, basePath + File.separator + "crl" + File.separator + simpleDateFormat.format(date) + index[0] + ".crl");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            index[0]++;
        });
    }

    private static void downloadCrl(URIName uri, String outpath) throws IOException {
        URL url = new URL(uri.getURI().toString());
        HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
        InputStream netinput = httpURLConnection.getInputStream();
        // 判断路径是否存在
        File outfile = new File(outpath);
        if (!outfile.exists()) {
            createFileAndDir(outfile);
        }
        FileOutputStream fileOutputStream = new FileOutputStream(outpath);

        // 返回数据
        int index;
        byte[] bytes = new byte[2048];
        while ((index = netinput.read(bytes)) != -1) {
            // 写出数据集
            fileOutputStream.write(bytes, 0, index);
        }
        fileOutputStream.flush();
    }

    // 创建路径和文件
    private static void createFileAndDir(File file) throws IOException {
        if (file.exists()) {
            return;
        }

        // 如果不存在
        if (file.isDirectory()) {
            file.mkdirs();
        }

        // 如果不是目录
        if (file.getParentFile().exists()) {
            file.createNewFile();
        }

        // 如果目录都不存在
        //创建上级目录
        file.getParentFile().mkdirs();
        file.createNewFile();
    }

    private static boolean needInstall(SSLSocketFactory sslSocketFactory, String host, int port) throws IOException {
        boolean needInstall = false;
        SSLSocket sslSocket = null;
        try {
            sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port);
            sslSocket.setSoTimeout(3000);
            sslSocket.startHandshake();
            sslSocket.close();
        } catch (SSLException e) {
//            LOGGER.info("", e);
            needInstall = true;
        } finally {
            sslSocket.close();
        }
        return needInstall;
    }

    /**
     * 防止 replaceCRLF注入
     *
     * @param message 参数
     * @return str
     */
    public static String replaceCRLF(String message) {
        if (StringUtils.isBlank(message)) {
            return "";
        }
        return message.replace('\n', '_').replace('\r', '_');
    }

}

网站实验

@Test
	public void testHttpsNew() throws UnsupportedEncodingException {
		// https://github.com/c
		// https://t.bilibili.com/
		Map params = new HashMap();
		params.put("host", "api.bilibili.com");
		params.put("port", 443);
		params.put("sslVersion", "TLSv1.2");
		params.put("protocol", "https");
		params.put("urlPath", "/");
		params.put("timeOut", 3000);
		params.put("fileName", "test.jks");
		params.put("certsPasswd", "changit");
//		sourcePasswd2
		params.put("sourcePasswd", "changit");
		params.put("jdkcertPasswd", "changeit");
		params.put("templateHome", "C:\\Users\\Administrator\\Desktop\\tmp\\https");
		params.put("revoked", "diaoxiao.crl");
		params.put("requestMethod", "GET");
		params.put("url", "https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all?timezone_offset=-480&type=video&page=1");

		// 设置请求头,这里我用的我自己B站的回话,所以就隐藏一下
		Map heads = new HashMap();
		heads.put("cookie", "SESSDATA=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;");
		heads.put("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36");
		params.put("heads", heads);

		String res = HttpsUtil2022.sendPostWithCheckCertAndRevokedCheck(params);
		System.out.println(res);
	}

返回的结果:

最后说一句
其实这个代码如果debug就会发现,并没有走真正的证书校验的逻辑,但是我暂时又找不到不信任证书的网站,自己搭建的话又比较耗时间,所以这里就直接给出代码,大家可以线下验证一下,至于为什么不走,应该是走的互联网公证的CA证书可以直接连接,不需要走本地的证书校验了
还有一个就是证书吊销那块的逻辑需要说一下(分2种,一个CRL,一个OCSP):
证书吊销分2中,我这里是本地设置证书吊销库
这两种的差别是一个是本地校验,一个是远程网站接口校验,
1.如果本地校验,就需要远程下载一个crl库,然后进行比较,比较耗时
2.OCSP的话,就需要能通外网,通过外网的OCSP接口进行校验证书是否吊销,有网络要求
分别对应的在证书中2个扩展信息,这里B站的话就是第二种,第一种一般是自己制作的证书会走这种方式了
X.509 证书扩展:CRL Distribution Points
X.509 证书扩展:authorityInfoAccess

综上:
最好的版本其实应该就是提前下载/准备好CRL证书文件,然后本地直接一次性加载,后续就直接用,这样既不需要在认证的时候再去下载耗时,也不需要通外网进行接口认证

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章