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

带密钥证书的Curl转Spring Boot代码遇SSL证书匹配问题求助

SSL证书匹配异常问题排查与解决

问题背景

我执行以下Curl命令可正常运行:

curl --tlsv1.2 -k -iv -X POST -H "Content-Type:text/xml" --key node-key.key --cert node.crt  --data-raw 'PAYLOAD' https://IP_ADDRESS:PORT/uri -u "test:test"

随后我完成了以下操作:

  • 生成p12文件:
openssl pkcs12 -export -in node.crt -inkey node-key.key -out node-store.p12
  • 获取服务器自签证书并保存到node-self-sign.pem:
openssl s_client -connect IP_ADDRESS:PORT 2>/dev/null </dev/null |  sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'
  • 将自签证书导入生成jks文件:
keytool -keystore node-KeyStore.jks -alias selfsigncert -import -file node-self-sign.pem

在Spring Boot中配置SSL并发起请求:

初始化SSL上下文代码

@PostConstruct
public void initEcwConnection() {
    try {
        File cert = new File(ecwCertPath);
        SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial(jks,pass.toCharArray())
                .loadKeyMaterial(p12, pass.toCharArray(),pass.toCharArray())
                .build();
        CloseableHttpClient client = HttpClients.custom().setSSLContext(sslContext).build();
        requestFactory = new HttpComponentsClientHttpRequestFactory();
        requestFactory.setHttpClient(client);
    } catch (Exception exp) {
        LOGGER.error(exp.getMessage(), exp);
    }
}

RestTemplate请求代码

HttpHeaders headers = new HttpHeaders();
File file = new File("paybundle.xml");
FileInputStream fis = new FileInputStream(file);
String payload = new String(fis.readAllBytes());
HttpEntity<String> entity = new HttpEntity<String>(payload, headers);
ResponseEntity<String> response = ecwTemplate.exchange("https://IP_ADDRESS:8010/vsl/preapproval",HttpMethod.POST, entity,String.class);
System.out.println(response.getBody());

运行后抛出异常:

Caused by: javax.net.ssl.SSLPeerUnverifiedException: Certificate for <IP_ADDRESS> doesn't match any of the subject alternative names: []
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.verifyHostname(SSLConnectionSocketFactory.java:507) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:437) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:384) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:376) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:393) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) ~[httpclient-4.5.13.jar:4.5.13]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56) ~[httpclient-4.5.13.jar:4.5.13]
    at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:87) ~[spring-web-5.3.19.jar:5.3.19]
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) ~[spring-web-5.3.19.jar:5.3.19]
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66) ~[spring-web-5.3.19.jar:5.3.19]
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:776) ~[spring-web-5.3.19.jar:5.3.19]

解决方案

原因分析

异常明确说明服务器证书的主题备用名称(SAN)为空,HTTP客户端默认会验证请求的IP/域名是否匹配证书的SAN或CN字段。Curl命令能成功是因为加了-k参数(跳过SSL证书验证),但Spring Boot中的HttpClients默认开启严格的主机名验证。

解决方法

根据实际场景选择以下方案:

方案1:修改服务器证书,添加IP作为SAN字段(生产环境推荐)

如果有权限修改服务器证书,重新生成包含目标IP的自签证书:

# 生成证书请求时指定SAN字段
openssl req -new -key node-key.key -out node.csr -subj "/CN=IP_ADDRESS" -addext "subjectAltName=IP:IP_ADDRESS"
# 自签证书
openssl x509 -req -in node.csr -signkey node-key.key -out node.crt -days 365 -extfile <(echo "subjectAltName=IP:IP_ADDRESS")

之后重新生成p12文件,并更新jks中的服务器证书,代码无需修改即可正常通过验证。

方案2:客户端跳过主机名验证(仅测试环境使用)

如果无法修改服务器证书,可在Spring Boot中配置HttpClient跳过主机名验证:
修改initEcwConnection方法,添加NoopHostnameVerifier:

@PostConstruct
public void initEcwConnection() {
    try {
        File cert = new File(ecwCertPath);
        SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial(jks,pass.toCharArray())
                .loadKeyMaterial(p12, pass.toCharArray(),pass.toCharArray())
                .build();
        // 禁用主机名验证
        CloseableHttpClient client = HttpClients.custom()
                .setSSLContext(sslContext)
                .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
                .build();
        requestFactory = new HttpComponentsClientHttpRequestFactory();
        requestFactory.setHttpClient(client);
    } catch (Exception exp) {
        LOGGER.error(exp.getMessage(), exp);
    }
}

注意:该方案会降低安全性,仅适合测试环境使用。

方案3:本地hosts映射(仅证书CN为域名时适用)

如果服务器证书的CN是某个域名,但你用IP访问,可在本地hosts文件添加映射:

IP_ADDRESS  证书CN对应的域名

之后请求时用域名代替IP,即可通过主机名验证。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 05:35:29