Java SSL无SAN证书的IP主机验证问题及合规方案咨询
解决方案:自定义HostnameVerifier实现IP与证书CN的校验
核心思路
Java默认的主机名校验逻辑对IP地址强制要求证书包含SAN字段,无法通过CN匹配。我们可以通过自定义HostnameVerifier,针对IP地址场景手动校验证书CN与目标IP是否匹配,同时保留非IP地址的默认校验逻辑,既满足合规要求,又解决证书无SAN的问题。
实现步骤
1. 编写自定义HostnameVerifier
实现javax.net.ssl.HostnameVerifier接口,区分IP与非IP地址的校验逻辑:
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.regex.Pattern; public class IpCnHostnameVerifier implements HostnameVerifier { // IP地址匹配正则 private static final Pattern IP_PATTERN = Pattern.compile( "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"); // 保留默认校验器用于非IP场景 private final HostnameVerifier defaultVerifier; public IpCnHostnameVerifier(HostnameVerifier defaultVerifier) { this.defaultVerifier = defaultVerifier; } @Override public boolean verify(String hostname, SSLSession session) { // 处理IP地址场景:校验证书CN与IP是否一致 if (IP_PATTERN.matcher(hostname).matches()) { try { Certificate[] peerCerts = session.getPeerCertificates(); if (peerCerts.length > 0 && peerCerts[0] instanceof X509Certificate) { X509Certificate x509Cert = (X509Certificate) peerCerts[0]; String certCn = extractCommonName(x509Cert.getSubjectDN().getName()); return hostname.equals(certCn); } } catch (Exception e) { // 证书解析失败或无有效CN,返回校验失败 return false; } return false; } // 非IP地址用默认校验逻辑 return defaultVerifier.verify(hostname, session); } // 从证书DN中提取CN字段 private String extractCommonName(String distinguishedName) { String[] dnParts = distinguishedName.split(","); for (String part : dnParts) { String trimmedPart = part.trim(); if (trimmedPart.startsWith("CN=")) { return trimmedPart.substring(3); } } return null; } }
2. 在HTTPS请求中启用自定义校验器
根据你使用的HTTP客户端,设置自定义的HostnameVerifier:
示例1:HttpsURLConnection
import javax.net.ssl.HttpsURLConnection; import java.net.URL; public class HttpsRequestExample { public static void main(String[] args) throws Exception { URL url = new URL("https://193.321.1.321/customers"); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); // 设置自定义校验器,保留默认逻辑处理非IP地址 connection.setHostnameVerifier(new IpCnHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier())); // 执行POST请求 connection.setRequestMethod("POST"); connection.setDoOutput(true); // 后续请求体写入、响应处理逻辑... } }
示例2:Apache HttpClient
import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContexts; import javax.net.ssl.SSLContext; public class ApacheHttpClientExample { public static void main(String[] args) throws Exception { SSLContext sslContext = SSLContexts.createDefault(); SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( sslContext, new IpCnHostnameVerifier(SSLConnectionSocketFactory.getDefaultHostnameVerifier()) ); try (CloseableHttpClient client = HttpClients.custom() .setSSLSocketFactory(sslSocketFactory) .build()) { HttpPost post = new HttpPost("https://193.321.1.321/customers"); // 执行请求并处理响应... } } }
关于修改默认校验逻辑的说明
直接修改sun.security.util.HostnameChecker的isIpAddress方法属于操作JVM私有API,存在以下问题:
- 依赖JVM实现细节,不同版本的OpenJDK/Oracle JDK可能有差异,升级JVM后代码可能失效
- 违反Java规范,私有API随时可能被移除或修改
- 全局修改会影响所有HTTPS请求,风险不可控
因此不推荐这种方式,自定义HostnameVerifier是标准、可控的合规解决方案。
合规性说明
该方案没有完全跳过主机名校验,仅针对IP地址场景调整了匹配规则(用CN代替SAN),仍在验证证书与目标地址的关联性,符合"不跳过主机校验"的要求。但需确保证书的CN字段确实与目标IP一致,避免引入安全风险。
内容的提问来源于stack exchange,提问作者ROBLCSnail
相关产品推荐
相关产品推荐

