【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证书文件,然后本地直接一次性加载,后续就直接用,这样既不需要在认证的时候再去下载耗时,也不需要通外网进行接口认证