【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證書文件,然後本地直接一次性加載,後續就直接用,這樣既不需要在認證的時候再去下載耗時,也不需要通外網進行接口認證

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