说明:
获取https证书信息包括以下的方法:
- 通过whois命令,使用当前工具类执行,但是需要有whois的命令权限
- 调用域名获取证书相关信息
1、测试入口
@Test
public void test() {
TslDTO tslDTO = TlsUtils.getFirstTlsInfo("https://www.baidu.com");
System.out.println(JSON.toJSONString(tslDTO));
TslByShellDTO tslByShellDTO = TlsUtils.getFirstTlsByShell("https://www.sina.com");
System.out.println(JSON.toJSONString(tslByShellDTO));
Assert.assertTrue(ObjectUtils.anyNotNull(tslByShellDTO));
}
2、注解
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
/**
* 属性转换的值
*
* @author wangmingcong
* @date 2021/4/23
*/
@Documented
@Target(value = {FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldValue {
/**
* 属性值
*
* @return 属性值
*/
String value();
}
3、实体类
import xxxxxx.annotation.FieldValue;
import lombok.Data;
import java.util.Date;
/**
* @author wangmingcong
* @date 2021/4/25
*/
@Data
public class TslByShellDTO {
/**
* 服务器 Tengine
*/
@FieldValue(value = "server")
private String server;
/**
* Sun, 25 Apr 2021 06:35:47 GMT
*/
@FieldValue(value = "date")
private Date date;
/**
* text/html; charset=utf-8
*/
@FieldValue(value = "content-type")
private String contentType;
/**
* 472997
*/
@FieldValue(value = "content-length")
private String contentLength;
/**
* 1; mode=block
*/
@FieldValue(value = "x-xss-protection")
private String xXssProtection;
/**
* nosniff
*/
@FieldValue(value = "x-content-type-options")
private String xContentTypeOptions;
/**
* noopen
*/
@FieldValue(value = "x-download-options")
private String xDownloadOptions;
/**
* 275
*/
@FieldValue(value = "x-readtime")
private String xReadTime;
/**
* Sun,25Apr 2021 06:38:47GMT
*/
@FieldValue(value = "expires")
private Date expires;
/**
* Nov 30 00:00:00 2020 GMT
*/
@FieldValue(value = "* start date")
private Date startDate;
/**
* Dec 31 23:59:59 2021 GMT
*/
@FieldValue(value = "* expire date")
private Date expireDate;
/**
* max-age=180
*/
@FieldValue(value = "cache-control")
private String cacheControl;
/**
* HIT
*/
@FieldValue(value = "cache-status")
private String cacheStatus;
/**
* www.9ji.com/'homessr'
*/
@FieldValue(value = "cache-status1")
private String cacheStatus1;
}
import lombok.Data;
import java.math.BigInteger;
import java.security.PublicKey;
import java.util.Date;
/**
* @author wangmingcong
* @date 2021/4/25
*/
@Data
public class TslDTO {
/**
* 版本号
*/
private Integer version;
/**
* 证书编号
*/
private BigInteger serialNumber;
/**
* 颁发机构
*/
private String subjectDNName;
/**
* 颁发者
*/
private String issuerDNName;
/**
* 证书开始时间
*/
private Date notBefore;
/**
* 有效期止时间
*/
private Date notAfter;
/**
* 签名算法
*/
private String sigAlgName;
/**
* 签名算法
*/
private PublicKey publicKey;
/**
* 证书签名
*/
private byte[] signature;
}
4、Http请求工具类
import javax.net.ssl.*;
import java.io.IOException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.util.HashMap;
import java.util.Map;
/**
* 功能不满足,扩展之前的接口
*
* @author wangmingcong
* @date 2021/4/25
*/
public class HttpTlsUtils {
private static final HashMap<String, String> HEADER_GET = new HashMap<>();
static {
HEADER_GET.put("Content-Type", "application/x-www-form-urlencoded");
}
/**
* 获取 TSL 的信息
*
* @param httpsUrl 请求地址
* @return 返回 TSL 的信息
*/
public static Certificate[] getCertificate(String httpsUrl) {
HttpsURLConnection conn = null;
try {
HttpsURLConnection.setDefaultHostnameVerifier(new HttpTlsUtils().new NullHostNameVerifier());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, getTrustManager(), new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
URL url = new URL(httpsUrl);
conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("GET");
for (Map.Entry<String, String> entry : HEADER_GET.entrySet()) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
conn.connect();
return conn.getServerCertificates();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
e.printStackTrace();
} catch (IOException ioException) {
ioException.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
return new Certificate[0];
}
/**
* getTrustManager
*
* @return getTrustManager
*/
private static TrustManager[] getTrustManager() {
return new TrustManager[]{new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
}};
}
/**
* NullHostNameVerifier
*/
private class NullHostNameVerifier implements HostnameVerifier {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
}
}
Shell执行工具类
https://blog.csdn.net/qq_38428623/article/details/116945589
5、主要工具类
import xxxx.common.exception.CustomizeException;
import xxxx.utils.ShellUtils;
import xxxx.utils.tls.dto.TslByShellDTO;
import xxxx.utils.tls.dto.TslDTO;
import xxxx.utils.whois.WhoisCommonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.util.CollectionUtils;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 获取 证书显相关的工具类
*
* @author wangmingcong
* @date 2021/4/25
*/
@Slf4j
public class TlsUtils {
/**
* 私有构造函数
*/
private TlsUtils() {
}
/**
* 获取 第一个证书的信息
*
* @param httpsUrl 必须是 https 的域名
* @return 返回 证书的相关信息
*/
public static TslDTO getFirstTlsInfo(String httpsUrl) {
Certificate[] certificates = HttpTlsUtils.getCertificate(httpsUrl);
if (ArrayUtils.isEmpty(certificates)) {
return null;
}
return convertTslDTO((X509Certificate) certificates[0]);
}
/**
* 获取 所有的Tls信息
*
* @param httpsUrl 必须是 https 的域名
* @return 返回 所有的Tls信息
*/
public static List<TslDTO> getAllTlsInfo(String httpsUrl) {
Certificate[] certificates = HttpTlsUtils.getCertificate(httpsUrl);
if (ArrayUtils.isEmpty(certificates)) {
return Collections.emptyList();
}
List<TslDTO> tslDTOList = new ArrayList<>();
for (Certificate certificate : certificates) {
if (certificate == null) {
continue;
}
tslDTOList.add(convertTslDTO((X509Certificate) certificate));
}
return tslDTOList;
}
/**
* 通过shell 方法获取 TSL 信息
* 有点问题,后面优化
*
* @param httpsUrl https 地址
* @return 返回 方法获取 TSL 信息
*/
public static TslByShellDTO getFirstTlsByShell(String httpsUrl) {
List<String> processList = new ArrayList<>();
try {
String shellStr = String.format("%s %s", "curl -v -I -k ", httpsUrl);
List<String> resultList = ShellUtils.getInputResult(shellStr);
processList.addAll(resultList);
List<String> errorResultList = ShellUtils.getErrorResult(shellStr);
processList.addAll(errorResultList);
} catch (Exception e) {
if (e instanceof InterruptedException) {
throw new CustomizeException(e.getMessage());
}
log.error("{}", e);
}
return convertTslByShellDTO(processList);
}
/**
* 转为 TslByShellDTO对象
*
* @param processList list 数据
* @return 返回 转换之后的数据
*/
private static TslByShellDTO convertTslByShellDTO(List<String> processList) {
if (CollectionUtils.isEmpty(processList)) {
return null;
}
TslByShellDTO tslByShellDTO = new TslByShellDTO();
for (String process : processList) {
WhoisCommonUtil.validateFiled(tslByShellDTO, process);
}
return tslByShellDTO;
}
// ----- 私有方法 -----
/**
* 转换为 TslDTO
*
* @param certificate x509Certificate
* @return 返回 TslDTO
*/
private static TslDTO convertTslDTO(X509Certificate certificate) {
TslDTO tslDTO = new TslDTO();
tslDTO.setVersion(certificate.getVersion());
tslDTO.setSerialNumber(certificate.getSerialNumber());
tslDTO.setSubjectDNName(certificate.getSubjectDN().getName());
tslDTO.setIssuerDNName(certificate.getIssuerDN().getName());
tslDTO.setNotBefore(certificate.getNotBefore());
tslDTO.setNotAfter(certificate.getNotAfter());
tslDTO.setSigAlgName(certificate.getSigAlgName());
tslDTO.setPublicKey(certificate.getPublicKey());
tslDTO.setSignature(certificate.getSignature());
return tslDTO;
}
}
import cn.hutool.http.HttpUtil;
import xxxx.annotation.FieldValue;
import io.vertx.core.impl.ConcurrentHashSet;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.reflections.ReflectionUtils;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Whois 解析结果
*
* @author wangmingcong
* @date 2021/4/23
*/
@Slf4j
@Data
public class WhoisCommonUtil {
/**
* 静态的变量
*/
public static final String EMPTY_STRING, COLON_STRING, URL_DOMAIN_FORMAT;
/**
* 时间格式,为了兼容
*/
private static final ConcurrentHashSet<String> EN_DATE_FORMAT = new ConcurrentHashSet<>();
static {
EMPTY_STRING = "";
COLON_STRING = ":";
URL_DOMAIN_FORMAT = "%s%s";
EN_DATE_FORMAT.add("yyyy-MM-dd");
EN_DATE_FORMAT.add("yyyy-MM-dd HH:mm:ss");
EN_DATE_FORMAT.add("EEE, dd MMM yyyy hh:mm:ss");
EN_DATE_FORMAT.add("EEE, dd MMM yyyy hh:mm:ss z");
EN_DATE_FORMAT.add("EEE MMM dd yyyy hh:mm:ss");
EN_DATE_FORMAT.add("EEE MMM dd yyyy hh:mm:ss z");
//Nov 30 00:00:00 2020 GMT
EN_DATE_FORMAT.add("MMM dd hh:mm:ss yyyy z");
}
private WhoisCommonUtil() {
}
/**
* 获取 whois的内容
*
* @param url 地址
* @return 返回 whois的内容
*/
public static String getWhoisContent(String url) {
return getWhoisContent(url, "");
}
/**
* 获取 whois的内容
*
* @param url 地址
* @param domainName 域名
* @return 返回 whois的内容
*/
public static String getWhoisContent(String url, String domainName) {
try {
return HttpUtil.get(String.format(URL_DOMAIN_FORMAT, url, domainName), 3000);
} catch (Exception e) {
log.error("", e);
return null;
}
}
/**
* 获取 匹配的结果
*
* @param content 待匹配的内容
* @param pattern 正则表达式
* @return 返回 匹配的结果
*/
public static List<String> getMatchResult(String content, String pattern) {
if (StringUtils.isBlank(content) || StringUtils.isBlank(pattern)) {
return Collections.emptyList();
}
Pattern r = Pattern.compile(pattern);
//创建 matcher 对象
Matcher m = r.matcher(content);
List<String> patternList = new ArrayList<>();
while (m.find()) {
patternList.add(m.group());
}
return patternList;
}
/**
* 返回 替换的内容
*
* @param content 待替换的内容
* @param pattern 正则表达式
* @return 返回 替换之后的内容
*/
public static String getReplaceContent(String content, String pattern) {
return getReplaceContent(content, pattern, EMPTY_STRING);
}
/**
* 返回 替换的内容
*
* @param content 待替换的内容
* @param pattern 正则表达式
* @param newContent 替换的字符串
* @return 返回 替换之后的内容
*/
public static String getReplaceContent(String content, String pattern, String newContent) {
if (StringUtils.isBlank(content) || StringUtils.isBlank(pattern) || StringUtils.isBlank(newContent)) {
return "";
}
return content.replaceAll(pattern, newContent);
}
/**
* 属性验证
*
* @param obj 对象
* @param content 内容
* @param <T> 泛型
*/
public static <T> void validateFiled(T obj, String content) {
if (StringUtils.isBlank(content)) {
return;
}
// 特殊处理时间的问题,因为时间的分隔符也是冒号 :,所以使用substring,不能使用 split
int indexOf = content.indexOf(WhoisCommonUtil.COLON_STRING);
if (indexOf == -1) {
return;
}
// 名称
Object fileName = content.substring(0, indexOf).trim();
// 值
Object fileValue = content.substring(indexOf + 1).trim();
// 验证并且赋值
WhoisCommonUtil.validateFiled(obj, fileName, fileValue);
}
/**
* 属性验证
*
* @param obj 域名返回的地址
* @param fileName 属性名
* @param fileValue 属性对应的值
*/
public static <T> void validateFiled(T obj, Object fileName, Object fileValue) {
// 获取 所有的 属性信息
Set<Field> fieldSet = ReflectionUtils.getAllFields(obj.getClass());
for (Field field : fieldSet) {
FieldValue fieldAnnotation = field.getAnnotation(FieldValue.class);
if (fieldAnnotation.value().equals(fileName)) {
validateField(obj, fileValue, field);
break;
}
}
}
/**
* 验证并且赋值
*
* @param obj obj 对象
* @param fileValue 属性的值
* @param field 属性
*/
private static <T> void validateField(T obj, Object fileValue, Field field) {
Class<?> fieldType = field.getType();
if (!isPrimitive(fieldType)) {
// 获取值
Object objValue = null;
try {
field.setAccessible(true);
objValue = field.get(obj);
} catch (IllegalAccessException e) {
log.error("{}", e);
}
// 假如是 List
if (List.class.equals(fieldType)) {
List<Object> list = (List) objValue;
if (CollectionUtils.isEmpty(list)) {
list = new ArrayList<>();
}
list.add(fileValue);
fileValue = list;
}
}
// 赋值
setFieldValue(obj, fileValue, field);
}
/**
* 属性赋值
*
* @param obj 域名返回的结果
* @param fileValue 属性值
* @param field 属性
*/
private static <T> void setFieldValue(T obj, Object fileValue, Field field) {
// 转换值的格式
fileValue = convertValueFormat(field, fileValue);
try {
field.setAccessible(true);
field.set(obj, fileValue);
} catch (IllegalAccessException e) {
log.error("{}", e);
}
}
/**
* 根据 属性的类型,转换对应的值
*
* @param field 属性
* @param fileValue 属性值
* @return 返回 属性的类型,转换对应的值
*/
private static Object convertValueFormat(Field field, Object fileValue) {
if (field.getType().equals(Date.class)) {
String value = fileValue.toString();
for (String dateFormat : EN_DATE_FORMAT) {
try {
fileValue = DateUtils.parseDate(value, dateFormat);
break;
} catch (ParseException e) {
SimpleDateFormat sf = new SimpleDateFormat(dateFormat, Locale.ENGLISH);
try {
fileValue = sf.parse(value);
break;
} catch (ParseException parseException) {
continue;
}
}
}
}
return fileValue;
}
/**
* 验证 是否是基本类型或者基本类型的包装类
*
* @param fieldType 类型
* @return 返回 是否是基本类型或者基本类型的包装类
*/
private static boolean isPrimitive(Class<?> fieldType) {
if (fieldType.isPrimitive()) {
return true;
}
return fieldType.equals(String.class)
|| fieldType.equals(Double.class)
|| fieldType.equals(Float.class)
|| fieldType.equals(Long.class)
|| fieldType.equals(Integer.class)
|| fieldType.equals(Short.class)
|| fieldType.equals(Character.class);
}
}
6、执行结果
{"issuerDNName":"CN=GlobalSign Organization Validation CA - SHA256 - G2, O=GlobalSign nv-sa, C=BE","notAfter":1627277462000,"notBefore":1585811098000,"publicKey":{"algorithm":"RSA","algorithmId":{"name":"RSA","oID":{}},"encoded":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwamwrkca0lfrHRUfblyy5PgLINvqAN8p/6RriSZLnyMv7FewirhGQCp+vNxaRZdPrUEOvCCGSwxdVSFH4jE8V6fsmUfrRw1y18gWVHXv00URD0vOYHpGXCh0ro4bvthwZnuok0ko0qN2lFXefCfyD/eYDK2G2sau/Z/w2YEympfjIe4EkpbkeBHlxBAOEDF6Speg68ebxNqJN6nDN9dWsX9Sx9kmCtavOBaxbftzebFoeQOQ64h7jEiRmFGlB5SGpXhGeY9Ym+k1Wafxe1cxCpDPJM4NJOeSsmrp5pY3Crh8hy900lzoSwpfZhinQYbPJqYIjqVJF5JTs5Glz1OwMQIDAQAB","encodedInternal":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwamwrkca0lfrHRUfblyy5PgLINvqAN8p/6RriSZLnyMv7FewirhGQCp+vNxaRZdPrUEOvCCGSwxdVSFH4jE8V6fsmUfrRw1y18gWVHXv00URD0vOYHpGXCh0ro4bvthwZnuok0ko0qN2lFXefCfyD/eYDK2G2sau/Z/w2YEympfjIe4EkpbkeBHlxBAOEDF6Speg68ebxNqJN6nDN9dWsX9Sx9kmCtavOBaxbftzebFoeQOQ64h7jEiRmFGlB5SGpXhGeY9Ym+k1Wafxe1cxCpDPJM4NJOeSsmrp5pY3Crh8hy900lzoSwpfZhinQYbPJqYIjqVJF5JTs5Glz1OwMQIDAQAB","format":"X.509","modulus":24447670194681134930622535078849307650955709445907141319429480038436215335358073627900931159797668335850497404378455565832224636917345203491168006207927092349898005019173027938424401309836372580357513567321708170696086150650622138642758058891488241515864554714488540371116467529413156897546991986743817547978784330145647870682680887376850278609713886918023141693015453400702875454035455875710531226071413246691218088873710308709233736395323607087215365814260642368927749970191077661316040264473990918611638306735542549194843604547458738232510083862807640123860703823935330939847871556783613794587557047453884618092593,"publicExponent":65537},"serialNumber":35388244279832734960132917320,"sigAlgName":"SHA256withRSA","signature":"vNwC0NnejMXi2f5N77rRIos0QlmEkjGC1Qq8QDXbBrITbsjPAfFfwOe3NDc6qAjynzLV+SCAn7/T/21HnHbRy/HH8duDMzflPxinAOK92v5PKUVXh3hfU4UNs6NcY5P+4CZe+ZKM7XajXznmIgU2xTJz0M1RqsjDH6isWya32ZRgCIGB0/W3ek/fOSFYM7UVYwKMuCLq2Xp07FpBuz2nyeJAIeo0Gkrtc2BGx5Y7meT15ZITzvQ8FtViD7oOma5cpS002JpVt1hEzgE4u9B2LGTejQArmeLdYRDtwLBe5ao3QNh8EzddBV9h7mlL3+Tsz/jyrqVfVSsPMfJkClOr6w==","subjectDNName":"CN=baidu.com, O=\"Beijing Baidu Netcom Science Technology Co., Ltd\", OU=service operation department, L=beijing, ST=beijing, C=CN","version":3}
{"cacheControl":"max-age=120","contentLength":"24000","contentType":"text/html","date":1621180947000,"expires":1621180962000,"server":"Tengine"}