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