You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于X509Certificate对象实现证书主机名验证?

验证X509Certificate与主机名的匹配

如果你不想依赖SSLSession直接验证证书和主机名,有两种可行方案:

方案一:手动实现验证逻辑

手动解析证书的SAN(Subject Alternative Name)和CN(Common Name)字段,实现符合RFC规范的匹配逻辑:

1. 解析证书的SAN DNS条目

import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

private static List<String> getSanDnsNames(X509Certificate cert) throws CertificateParsingException {
    List<String> sanDnsList = new ArrayList<>();
    Collection<List<?>> subjectAltNames = cert.getSubjectAlternativeNames();
    if (subjectAltNames == null) {
        return sanDnsList;
    }
    for (List<?> sanEntry : subjectAltNames) {
        if (sanEntry.size() >= 2) {
            Integer type = (Integer) sanEntry.get(0);
            // 类型2对应DNS名称
            if (type == 2) {
                sanDnsList.add((String) sanEntry.get(1));
            }
        }
    }
    return sanDnsList;
}

2. 解析证书的Common Name

import java.security.cert.X509Certificate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

private static String getCertificateCommonName(X509Certificate cert) {
    String subjectDn = cert.getSubjectX500Principal().getName();
    Pattern cnPattern = Pattern.compile("CN=([^,]+)", Pattern.CASE_INSENSITIVE);
    Matcher matcher = cnPattern.matcher(subjectDn);
    if (matcher.find()) {
        return matcher.group(1).trim();
    }
    return null;
}

3. 实现主机名匹配逻辑

import java.util.List;
import java.util.Locale;

public static boolean verifyCertificateHostname(String hostname, X509Certificate cert) throws CertificateParsingException {
    List<String> sanDnsNames = getSanDnsNames(cert);
    
    // 优先检查SAN字段(符合RFC 6125规范)
    if (!sanDnsNames.isEmpty()) {
        for (String sanDns : sanDnsNames) {
            if (matchesHostnamePattern(hostname, sanDns)) {
                return true;
            }
        }
    } else {
        // SAN字段为空时,回退检查CN字段
        String commonName = getCertificateCommonName(cert);
        if (commonName != null && matchesHostnamePattern(hostname, commonName)) {
            return true;
        }
    }
    return false;
}

private static boolean matchesHostnamePattern(String hostname, String pattern) {
    // 忽略大小写,统一转小写
    String lowerHostname = hostname.toLowerCase(Locale.ROOT);
    String lowerPattern = pattern.toLowerCase(Locale.ROOT);
    
    // 处理通配符规则:仅匹配一级子域名,不能匹配根域名或多级子域名
    if (lowerPattern.startsWith("*.")) {
        String domainSuffix = lowerPattern.substring(2);
        return lowerHostname.endsWith(domainSuffix) 
                && lowerHostname.split("\\.").length == domainSuffix.split("\\.").length + 1;
    } else {
        // 精确匹配
        return lowerHostname.equals(lowerPattern);
    }
}

方案二:使用Apache HttpClient工具类

Apache HttpClient的DefaultHostnameVerifier已经封装了完整的证书主机名验证逻辑,支持直接传入X509Certificate对象:

1. 引入依赖(Maven)

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.14</version> <!-- 可替换为最新稳定版 -->
</dependency>

2. 调用验证方法

import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import java.security.cert.X509Certificate;

// 创建验证器
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier();
// 直接验证主机名与证书
boolean isMatch = hostnameVerifier.verify(targetHostname, targetCertificate);

注意事项

  • SAN优先原则:根据RFC 6125规范,证书的SAN字段优先级高于CN字段,验证时必须优先检查SAN。
  • 通配符限制:合法的通配符仅能匹配一级子域名(如*.example.com匹配a.example.com,但不匹配example.com或a.b.example.com)。
  • 国际化域名(IDN):如果涉及非ASCII域名,需要先转换为Punycode格式再验证,Apache的工具类已自动处理该场景,手动实现时需额外添加IDN转换逻辑。

内容的提问来源于stack exchange,提问作者Ido

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 11:17:48