编程语言

No subject alternative names present(Java)怎么修

作者: · 最后核实

报错信息java.security.cert.CertificateException: No subject alternative names present

No subject alternative names present 的意思是:Java 要拿你访问的地址去比对证书的 SAN(使用者可选名称)扩展,结果证书里没有 SAN。 实际场景几乎都是用 IP 访问(比如 https://203.0.113.10:8443),而证书只写了域名;或者是只有 Common Name 的老式自签名证书。

快速修复

最快的修法是改用证书 SAN 里的域名访问。先看证书的 SAN 列表:

openssl s_client -connect 203.0.113.10:8443 </dev/null 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
  • 列出了 DNS:api.example.com:改用这个域名访问,不要用 IP。还没有解析记录就在 DNSPod、阿里云解析里加一条(测试时也可以改 /etc/hosts)。
  • 完全没有 SAN,或者必须用 IP 访问:重新签发证书,把 IP 写进 SAN:
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
  -keyout server.key -out server.crt -subj "/CN=api.internal.example.com" \
  -addext "subjectAltName=DNS:api.internal.example.com,IP:203.0.113.10"

上面是自签名示例;生产环境建议用公司内部 CA 签发,客户端只需要信任一张根证书。

原因

最常见的情况是用 IP 地址访问一张只写了域名的证书。

1. 用 IP 访问只写了域名的证书

Java 的 HTTPS 主机名校验只拿 IP 去匹配 SAN 里的 IP Address 条目,不会拿它比对 DNS 名称或 CN。对接第三方接口时,对方只给了一个 IP 加端口,是这个报错最常见的来源。

  • 确认: 代码或配置里的 URL 是 IP;证书里只有 DNS 类型的 SAN。
  • 修复: 改用域名。在 Kubernetes、Docker 里用服务名(Service DNS),不要用 Pod 或容器 IP。

2. 只有 CN 的老式证书

很多年前生成的自签名证书、内部证书只有 CN=...,没有 SAN 扩展,越来越多客户端已不再接受。

  • 确认: openssl x509 -noout -ext subjectAltName 什么都不输出。
  • 修复: 带 SAN 重新签发。用 keytool:
keytool -genkeypair -alias server -keyalg EC -groupname secp256r1 -validity 365 \
  -dname "CN=api.internal.example.com" \
  -ext "SAN=dns:api.internal.example.com,ip:203.0.113.10" \
  -keystore server.p12 -storetype PKCS12

3. SAN 里的 IP 不对

如果报错是 No subject alternative names matching IP address 203.0.113.10 found,说明证书有 SAN,但不包含这个 IP,比如服务器迁移到了新的公网 IP。把新 IP 加进去,或者改用域名。

4. 域名不匹配的变体

No subject alternative DNS name matching api.example.com found 是同一项检查的域名版本:证书签给了别的名字。用正确的域名重新签发,可以先用 CSR 生成器确认 SAN 列表完整。

各环境修法

各种 Java HTTP 客户端用同一套主机名校验规则,修法都是改用证书里的名字,或者重签带对应 SAN 的证书。

Spring RestTemplate / WebClient、Apache HttpClient、OkHttp: 规则完全相同,把 base URL 改成域名,不要塞一个空的 HostnameVerifier。

JDBC 和消息队列: PostgreSQL、MySQL、Kafka、RabbitMQ 客户端开启主机名校验后也会报类似错误,连接串要用证书里的名字。

Kubernetes: API Server、Webhook 的证书 SAN 里要包含 Service 域名(如 my-svc.my-namespace.svc),用 cert-manager 时写在 dnsNames 字段里。

对接第三方只给 IP 的接口: 先问对方要域名;对方确实只有 IP 时,请对方换一张把该 IP 写进 SAN 的证书。不要在自己代码里为这一个接口关掉主机名校验,否则同一个 HttpClient 实例访问其他接口时也失去了保护。

宝塔面板和内网测试环境: 内网测试服务也建议从一开始就带 SAN 生成证书,可以用自签名证书生成器,生成后用证书解析工具核对 SAN。

不要这样做

关闭主机名校验后报错会消失,但任何证书都能冒充你的服务器。

  • HttpsURLConnection.setDefaultHostnameVerifier((h, s) -> true)。 这会关闭整个 JVM 的主机名校验,而不只是你那一次调用。
  • 给 java.net.http 客户端加 -Djdk.internal.httpclient.disableHostnameVerification。 这是内部测试开关,同样关掉了这层保护。
  • 把服务器的站点证书导入信任库来“修复”。 这个报错与信任无关,是名字不匹配,导入了也照样报。

常见问题

为什么用域名能连,用 IP 就报错?

用域名连接时,Java 拿域名去比对证书 SAN 里的 DNS 条目;用 IP 连接时,它只找 SAN 里的 IP 条目。普通的 example.com 证书没有 IP 条目,所以 IP 连接失败。

公网 CA 能给 IP 地址签证书吗?

部分可以。Let's Encrypt 官方文档列出只有 shortlived 配置支持 IP 标识,证书有效期约 160 小时,必须全自动续期。10.x、192.168.x 这类内网地址任何公网 CA 都不能签,只能用内部 CA。

Java 还会回退去匹配 Common Name 吗?

对域名,Java 的 HTTPS 主机名校验在证书没有 DNS 类型 SAN 时仍可能回退到 CN,但 Go、Chrome 等客户端早已不再这样做。对 IP,Java 要求必须有 IP 类型 SAN。所有域名和 IP 都应写进 SAN。

内网服务写一个总返回 true 的 HostnameVerifier 可以吗?

不可以。关掉主机名校验后,任何由你信任的 CA 签发的证书都能冒充任意服务,一张内网证书泄露就能冒充所有服务。应该修证书。

资料来源

  1. Java Secure Socket Extension (JSSE) Reference Guide
  2. keytool (Java SE 21 tool reference)
  3. RFC 5280: Internet X.509 PKI Certificate and CRL Profile
  4. Let's Encrypt: Profiles