一文解決Java中IP地址訪問HTTPS接口的SSL證書驗證問題
問題描述
在使用Java的RestTemplate訪問HTTPS接口時,如果使用IP地址而不是域名,經常會遇到以下錯誤:
java.security.cert.CertificateException: No subject alternative names matching IP address 111.63.81.79 found
nested exception is javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative names matching IP address 111.63.81.79 found
奇怪的是:使用Postman、curl等工具可以正常訪問,但Java代碼卻報錯。
問題原因
SSL證書驗證機制
HTTPS協(xié)議在建立連接時,會進行SSL證書驗證,主要包括兩個方面:
- 證書有效性驗證:檢查證書是否過期、是否被吊銷等
- 主機名驗證:檢查證書中的域名或IP地址是否與訪問的地址匹配
為什么Postman可以,Java不行
- Postman:默認情況下,Postman可能會跳過某些SSL驗證(特別是開發(fā)環(huán)境)
- Java:Java的SSL驗證非常嚴格,會檢查證書的
Subject Alternative Names (SAN)字段 - 問題根源:當使用IP地址訪問時,如果證書的SAN中沒有包含該IP地址,Java就會拒絕連接
證書的Subject Alternative Names
SSL證書中有一個字段叫Subject Alternative Names (SAN),它列出了該證書可以用于哪些域名或IP地址。例如:
Subject Alternative Names:
DNS: example.com
DNS: www.example.com
IP Address: 192.168.1.1
如果證書中沒有包含你訪問的IP地址(如111.63.81.79),Java就會拋出上述異常。
解決方案
方案概述
我們需要配置RestTemplate,讓它跳過SSL證書驗證。注意:這種方法只適用于開發(fā)/測試環(huán)境,生產環(huán)境應該使用正確的證書。
完整代碼實現
創(chuàng)建一個RestTemplateConfig配置類:
package com.example.zbx.config;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
/**
* RestTemplate配置類
* 配置忽略SSL證書驗證,支持使用IP地址訪問HTTPS接口
* @author lxy
*/
@Configuration
public class RestTemplateConfig {
/**
* 創(chuàng)建RestTemplate Bean
* 配置忽略SSL證書驗證,支持使用IP地址訪問HTTPS接口
* @return RestTemplate實例
*/
@Bean
public RestTemplate restTemplate() {
try {
// 創(chuàng)建完全信任所有證書的TrustManager
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {
// 不進行任何檢查,信任所有客戶端證書
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
// 不進行任何檢查,信任所有服務器證書
}
}
};
// 創(chuàng)建SSL上下文,使用自定義的TrustManager
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
// 創(chuàng)建SSL連接工廠,忽略主機名驗證(包括IP地址驗證)
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE
);
// 創(chuàng)建請求配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(10000) // 連接超時10秒
.setConnectionRequestTimeout(10000) // 請求超時10秒
.setSocketTimeout(30000) // 讀取超時30秒
.build();
// 創(chuàng)建HttpClient
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(sslSocketFactory)
.setDefaultRequestConfig(requestConfig)
.evictExpiredConnections()
.evictIdleConnections(30, java.util.concurrent.TimeUnit.SECONDS)
.build();
// 創(chuàng)建請求工廠
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
factory.setConnectTimeout(10000);
factory.setConnectionRequestTimeout(10000);
factory.setReadTimeout(30000);
// 創(chuàng)建RestTemplate
return new RestTemplate(factory);
} catch (Exception e) {
throw new RuntimeException("創(chuàng)建RestTemplate失敗", e);
}
}
}
關鍵點說明
1. 自定義TrustManager
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
// 不進行任何檢查,信任所有服務器證書
}
// ... 其他方法
}
};
這個X509TrustManager會跳過所有證書驗證,包括:
- 證書是否過期
- 證書是否被吊銷
- 證書的域名/IP是否匹配
2. 創(chuàng)建SSL上下文
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
使用自定義的TrustManager初始化SSL上下文。
3. 忽略主機名驗證
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE // 不驗證主機名
);
NoopHostnameVerifier.INSTANCE會跳過主機名驗證,這樣即使證書中沒有對應的IP地址也能通過驗證。
Maven依賴
確保你的pom.xml中包含以下依賴:
<!-- HttpClient4 (for RestTemplate SSL configuration) -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
使用示例
配置完成后,直接使用RestTemplate即可,無需額外代碼:
@Autowired
private RestTemplate restTemplate;
public void callApi() {
String url = "https://111.63.81.79:10091/api/endpoint";
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
// 處理響應...
}
注意事項
安全警告
- 僅用于開發(fā)/測試環(huán)境:跳過SSL驗證會帶來安全風險,生產環(huán)境不應該使用
- 中間人攻擊風險:攻擊者可以偽造證書,你的應用無法識別
- 數據泄露風險:無法保證連接的安全性
生產環(huán)境建議
如果必須在生產環(huán)境使用,建議:
- 使用正確的證書:讓服務器提供包含IP地址的證書
- 配置證書信任:只信任特定的證書頒發(fā)機構(CA)
- 使用域名訪問:盡量使用域名而不是IP地址
示例:生產環(huán)境的正確做法
// 只信任特定的證書
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream("truststore.jks"), "password".toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
常見問題
Q1: 為什么之前可以,現在不行了?
可能的原因:
- Java版本更新,SSL驗證更嚴格了
- 服務器證書更新了,新證書沒有包含IP地址
- 網絡環(huán)境變化,使用了不同的IP地址
Q2: 有沒有更簡單的辦法?
對于開發(fā)環(huán)境,可以設置JVM參數(不推薦):
-Dcom.sun.net.ssl.checkRevocation=false
但這種方法不夠靈活,建議使用配置類的方式。
Q3: 會影響其他HTTP請求嗎?
不會。這個配置只影響使用RestTemplate的HTTPS請求,HTTP請求不受影響。
Q4: 如何驗證配置是否生效?
在代碼中添加日志,觀察是否還有證書驗證錯誤:
log.info("RestTemplate配置完成,SSL驗證已禁用");
如果不再出現CertificateException,說明配置生效了。
總結
- 問題根源:Java的SSL驗證嚴格,證書中沒有IP地址時會拒絕連接
- 解決方案:配置
RestTemplate跳過SSL證書驗證 - 關鍵代碼:自定義
X509TrustManager+NoopHostnameVerifier - 安全提醒:僅用于開發(fā)/測試環(huán)境,生產環(huán)境應使用正確的證書
到此這篇關于一文解決Java中IP地址訪問HTTPS接口的SSL證書驗證問題的文章就介紹到這了,更多相關Java解決SSL證書驗證問題內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

