# No subject alternative names present（Java）怎么修

> 用 IP 访问 HTTPS 而证书没有 SAN 时，Java 报 No subject alternative names present。改用证书里的域名，或重新签发带 IP SAN 的证书。

Source: https://ssl.sh/zh/errors/java-no-subject-alternative-names-present/
Last updated: 2026-11-10

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

## 快速修复

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

```bash
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**：

```bash
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：

```bash
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 生成器](https://ssl.sh/zh/tools/csr-generator/)确认 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 生成证书，可以用[自签名证书生成器](https://ssl.sh/zh/tools/self-signed-certificate-generator/)，生成后用[证书解析工具](https://ssl.sh/zh/tools/certificate-decoder/)核对 SAN。

## 不要这样做

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

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