Apache HttpClient 4.5.14证书SAN匹配问题及自定义验证需求
Apache HttpClient 4.5.14 证书SAN匹配问题解决方案
问题背景
使用Apache HttpClient 4.5.14时遇到证书匹配错误:
证书<sub1.sub2.sub3.domain.com>与任一主题备用名称(SAN)均不匹配:[ *.sub3.domain.com, SAN2, SAN3 ]
原因是默认证书验证遵循RFC 6125标准,通配符*仅支持匹配单级子域,无法覆盖多级子域场景。
1. 如何获取错误中显示的正确SAN列表?
你之前从本地信任库证书获取SAN失败,是因为实际握手使用的是服务器返回的证书,而非本地信任库中的副本。正确做法是从SSLSession中提取服务器端证书:
在自定义HostnameVerifier的verify方法中,通过session.getPeerCertificates()获取服务器证书,再解析SAN字段:
@Override public boolean verify(String hostname, SSLSession session) { try { // 获取服务器返回的叶证书 X509Certificate serverCert = (X509Certificate) session.getPeerCertificates()[0]; Collection<List<?>> sanEntries = serverCert.getSubjectAlternativeNames(); if (sanEntries != null) { for (List<?> entry : sanEntries) { // 类型标识为2的是DNS类型SAN if ((Integer) entry.get(0) == 2) { String san = (String) entry.get(1); // 此处可拿到错误提示中显示的SAN列表项 System.out.println("服务器证书SAN: " + san); } } } } catch (SSLException e) { e.printStackTrace(); } // 后续验证逻辑... return false; }
补充说明
- 若keytool检查本地信任库无
2.5.29.17扩展(即SAN字段)但错误提示存在SAN,说明本地信任库的证书与服务器实际返回证书不一致,需确认信任库是否正确导入服务器证书。
2. 配置允许*匹配多级子域
HttpClient无现成参数直接开启多级通配符匹配,需通过自定义HostnameVerifier实现逻辑,以下是完整可运行示例:
// 已初始化加载trustStore和keystore的SSLContext builder.setSSLContext(context); builder.setSSLHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { try { X509Certificate serverCert = (X509Certificate) session.getPeerCertificates()[0]; Collection<List<?>> sanEntries = serverCert.getSubjectAlternativeNames(); // 优先验证SAN字段 if (sanEntries != null) { for (List<?> entry : sanEntries) { if ((Integer) entry.get(0) == 2) { String san = (String) entry.get(1); if (matchDomain(hostname, san)) { return true; } } } } // 无SAN时回退验证证书CN字段 String cn = extractCommonName(serverCert.getSubjectX500Principal().getName()); return cn != null && matchDomain(hostname, cn); } catch (Exception e) { return false; } } // 自定义域名匹配逻辑,允许开头的*匹配多级子域 private boolean matchDomain(String hostname, String san) { if (hostname == null || san == null) return false; // 完全匹配 if (hostname.equalsIgnoreCase(san)) return true; // 仅处理开头为*.的通配符规则 if (san.startsWith("*.")) { String sanSuffix = san.substring(2); // 匹配规则:hostname以".sanSuffix"结尾,或直接等于sanSuffix(顶级域名场景) return hostname.endsWith("." + sanSuffix) || hostname.equalsIgnoreCase(sanSuffix); } return false; } // 从证书主题DN中提取CN字段 private String extractCommonName(String subjectDN) { String[] dnParts = subjectDN.split(","); for (String part : dnParts) { String trimmedPart = part.trim(); if (trimmedPart.startsWith("CN=")) { return trimmedPart.substring(3); } } return null; } });
安全提示
- 仅允许开头位置的
*通配符,避免任意位置通配符带来的安全风险; - 禁止使用
NoopHostnameVerifier.INSTANCE完全关闭验证,会暴露中间人攻击风险。
内容的提问来源于stack exchange,提问作者Rafał
相关产品推荐
相关产品推荐

