我最近将 LetsEncrypt 证书添加到我的服务器,但我的 Java 小程序在使用 TLS 连接时遇到问题。
我的小程序使用 Apache HttpClient。
我的网络服务器是 Apache 2,4,我有一些虚拟主机设置为我的主域的子域(foo.com - 不是我的真实域名)。
当我在暂存子域上运行我的小程序时(例如,它运行了https://staging.foo.com),我收到以下错误:
javax.net.ssl.SSLException: Certificate for <staging.foo.com> doesn't match any of the subject alternative names: [developer.foo.com]
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:165)
at org.apache.http.conn.ssl.BrowserCompatHostnameVerifier.verify(BrowserCompatHostnameVerifier.java:61)
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:141)
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:114)
at org.apache.http.conn.ssl.SSLSocketFactory.verifyHostname(SSLSocketFactory.java:580)
at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:554)
at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:412)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:179)
at org.apache.http.impl.conn.ManagedClientConnectionImpl.open(ManagedClientConnectionImpl.java:328)
at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:612)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:447)
at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:884)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107)
...(cut)
at javax.swing.SwingWorker$1.call(SwingWorker.java:295)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at javax.swing.SwingWorker.run(SwingWorker.java:334)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
我不知道发生了什么。
首先,我不知道Java如何知道developer.foo.bar是我的虚拟主机之一(尽管这个虚拟主机是按字母顺序排列的第一个打开了SSL的虚拟主机)。
我查看了 staging.foo.com 的证书详细信息,“主题备用名称”字段下列出的唯一名称是 staging.foo.com。
那么它从哪里获取developer.foo.com?
我该如何解决这个问题?
我在 OS X El Capitan 10.11.6 上使用 Firefox,并具有以下 Java 插件版本信息:
Java Plug-in 11.102.2.14 x86_64
Using JRE version 1.8.0_102-b14 Java HotSpot(TM) 64-Bit Server VM
这是 staging.foo.com 的 Apache conf 文件:
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName staging.foo.com
ServerAlias www.staging.foo.com
# Turn on HTTP Strict Transport Security (HSTS). This tells the
# client that it should only communicate with this site using
# HTTPS. See
# https://raymii.org/s/tutorials/HTTP_Strict_Transport_Security_for_Apache_NGINX_and_Lighttpd.html
Header always set Strict-Transport-Security "max-age=31536000; includeSubdomains;"
# The following is used to tunnel websocket requests to daphne, so
# that Django Channels can do its thing
ProxyPass "/ws/" "ws://localhost:8001/ws/"
ProxyPassReverse "/ws/" "ws://localhost:8001/ws/"
# The following is used during deployment. Every page request is
# served from one static html file.
RewriteEngine on
RewriteCond /home/www-mm/staging.foo.com/apache/in_maintenance -f
RewriteRule .* /home/www-mm/staging.foo.com/static/maintenance/maintenance.html
# Use Apache to serve protected (non-static) files. This is so that
# Apache can deal with ranges
XSendFile on
XSendFilePath /home/www-mm/staging.foo.com/user_assets
# Limit uploads - 200MB
LimitRequestBody 209715200
Alias /static/ /home/www-mm/staging.foo.com/serve_static/
Alias /robots.txt /home/www-mm/staging.foo.com/apache/serve-at-root/robots.txt
<Directory /home/www-mm/staging.foo.com/serve_static>
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json
Order deny,allow
Require all granted
</Directory>
# Videos uploaded via staff to home page should never cache,
# because they can change at any time (and we don't know if the
# URLs will change or not). Etags are used and only headers are
# sent if the files in question aren't modified (we get a 304
# back)
<Directory /home/www-mm/staging.foo.com/serve_static/video>
ExpiresActive On
# Expire immediately
ExpiresDefault A0
</Directory>
# The following ensures that the maintenance page is never cached.
<Directory /home/www-mm/staging.foo.com/static/maintenance>
ExpiresActive On
# Expire immediately
ExpiresDefault A0
Require all granted
</Directory>
# Hide uncompressed code from prying eyes. Python needs access to this code for the css compressor
<Directory /home/www-mm/staging.foo.com/serve_static/js/muso>
<Files ~ "\.js$">
Deny from all
</Files>
# Order deny,allow
# Deny from all
</Directory>
# Hide uncompressed code from prying eyes. Python needs access to this code for the css compressor
<DirectoryMatch "/home/www-mm/staging.foo.com/serve_static/js/dist/.*/muso">
Order deny,allow
Deny from all
</DirectoryMatch>
<Directory /home/www-mm/staging.foo.com/apache>
<Files django.wsgi>
Order deny,allow
Require all granted
</Files>
</Directory>
WSGIScriptAlias / /home/www-mm/staging.foo.com/apache/django.wsgi
WSGIDaemonProcess staging.foo.com user=www-mm group=www-mm
WSGIProcessGroup staging.foo.com
ErrorLog /var/log/apache2/staging.foo.com-error.log
CustomLog /var/log/apache2/staging.foo.com-access.log combined
SSLCertificateFile /etc/letsencrypt/live/staging.foo.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/staging.foo.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
SSL 部分是由 LetsEncrypt CLI 工具 certbot 添加的。
我应该补充一点,在现代浏览器(例如 Chrome)中访问每个子域都可以。
如果您使用HttpClient 4.4,那么您需要指定主机验证程序(NoopHostnameVerifier)以允许接受来自不同主机的证书:
SSLConnectionSocketFactory scsf = SSLConnectionSocketFactory(
SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(),
NoopHostnameVerifier.INSTANCE)
httpclient = HttpClients.custom().setSSLSocketFactory(scsf).build()
按照 Yurri 的评论,它通过在初始化 SSLConnectionSocketFactory 时添加 NoopHostnameVerifier.INSTANCE 解决了我的问题:
import org.apache.http.HttpHost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.TrustStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import java.net.Proxy;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.SSLContext;
/**
* Provide basic Utils for getting HttpHeader and making REST api calls.
*
*/
@Component
public class HttpUtil {
private static final Logger LOG = LoggerFactory.getLogger(HttpUtil.class);
/**
* The default implementation to get basic headers.
* @return HttpHeaders.
*/
public HttpHeaders getHttpHeaders(String userAgent, String host) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
headers.set(HttpHeaders.USER_AGENT, userAgent);
LOG.info("host=" + host);
if (null != host) {
headers.set(HttpHeaders.HOST, host);
}
return headers;
}
/**
* Default implementation to get RestTemplate
* @return
*/
public RestTemplate getRestTemplate(String proxyHost, int proxyPort)
throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
TrustStrategy acceptingTrustStrategy = new TrustSelfSignedStrategy();
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
if (null != proxyHost && proxyPort > 0) {
LOG.info("PROXY CONFIGURED | proxyHost=" + proxyHost + " | proxyPort=" + proxyPort);
HttpHost proxy = new HttpHost(proxyHost, proxyPort, Proxy.Type.HTTP.name());
httpClient = HttpClients.custom().setSSLSocketFactory(csf)
.setRoutePlanner(new DefaultProxyRoutePlanner(proxy)).build();
}
requestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
/**
* Make a rest api call
* @return ResponseEntity
*/
public ResponseEntity<String> getApiResponse(HttpMethod httpMethod, final String URL, final String userAgent,
String proxyHost, int proxyPort, String host) throws HttpClientErrorException {
ResponseEntity<String> response = null;
HttpEntity<String> httpEntity = new HttpEntity<>(getHttpHeaders(userAgent, host));
try {
if (null != httpMethod && null != URL) {
RestTemplate request = null;
try {
request = getRestTemplate(proxyHost, proxyPort);
response = request.exchange(URL, httpMethod, httpEntity, String.class);
} catch (KeyManagementException | KeyStoreException | NoSuchAlgorithmException e) {
LOG.error("Error creating Rest Template", e);
}
}
} catch (HttpClientErrorException ex) {
LOG.error("Method = " + httpMethod.toString() + "Request URL = " + URL);
LOG.error("Headers =" + getHttpHeaders(userAgent, host));
LOG.error("Response Status = " + ex.getStatusText());
LOG.error("Response Body = " + ex.getResponseBodyAsString());
throw ex;
}
return response;
}
}
我不知道您使用的是哪个版本的 Apache HttpClient,但版本 4.4.1 和 4.5.1 有一个错误,导致 SNI 无法正常工作。这已在 4.5.3 中修复
当您的主机/域名与您的证书 CN 名称不匹配时,您会收到此错误。 所以在这种情况下我们必须通过 NO_OP 关闭主机名验证(它在 httpclient 依赖大于 4.3 中可用)
示例代码:
SSLContext sslContextBuilder = SSLContextBuilder
.create()
.loadKeyMaterial(ResourceUtils.getFile("file:" + "/path/myclient.jks"), "123456".toCharArray(),"123456".toCharArray())
.loadTrustMaterial(ResourceUtils.getFile("file:" + "/path/myclient.jks"), "123456".toCharArray()).build();
CloseableHttpClient httpClient
= HttpClients.custom()
.setSSLHostnameVerifier(new NoopHostnameVerifier()).setSSLContext(sslContextBuilder)
.build();
final ClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
restTemplate.setRequestFactory(requestFactory);
如果您正在开发一个反应本机应用程序,并且当您尝试构建 Android 应用程序时会发生此问题(至少是我的情况!),这可能会有所帮助
这是一种黑客行为,而不是解决方案,但如果您只需要它立即工作,这就是它的方式。对我来说,问题是这样的:
Execution failed for task ':expo-modules-core:downloadBoost'.
> A failure occurred while executing de.undercouch.gradle.tasks.download.internal.DefaultWorkerExecutorHelper$DefaultWorkAction
> javax.net.ssl.SSLPeerUnverifiedException: Certificate for <jfrog-prod-usw2-shared-oregon-main.s3.amazonaws.com> doesn't match any of the subject alternative names: [*.s3.amazonaws.com, s3.amazonaws.com]
因此,我转到了模块
build.gradle
的expo-modules-core
文件。 (注意,不是您的根目录或应用程序 build.gradle
文件。
现在,我找到了
downloadBoost
任务(因为我的日志显示问题在于 downloadBoost
。对我来说,它看起来像这样:
def downloadBoost = tasks.create('downloadBoost', Download) {
dependsOn(createNativeDepsDirectories)
def srcUrl = REACT_NATIVE_TARGET_VERSION >= 69
? "https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION.replace("_", ".")}/source/boost_${BOOST_VERSION}.tar.gz"
: "https://github.com/react-native-community/boost-for-react-native/releases/download/v${BOOST_VERSION.replace("_", ".")}-0/boost_${BOOST_VERSION}.tar.gz"
src(srcUrl)
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz"))
}
我刚刚添加了一个 hack,以便它接受任何证书。添加这一行:
acceptAnyCertificate true
。所以,最终的块看起来像这样:
def downloadBoost = tasks.create('downloadBoost', Download) {
acceptAnyCertificate true
dependsOn(createNativeDepsDirectories)
def srcUrl = REACT_NATIVE_TARGET_VERSION >= 69
? "https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION.replace("_", ".")}/source/boost_${BOOST_VERSION}.tar.gz"
: "https://github.com/react-native-community/boost-for-react-native/releases/download/v${BOOST_VERSION.replace("_", ".")}-0/boost_${BOOST_VERSION}.tar.gz"
src(srcUrl)
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz"))
}
嗯,就是这样!事情对我有用。希望它对某人有帮助。
每次执行
yarn install
或npm install
时,build.gradle
文件都会自行重置,因此您需要手动再次执行。我建议为其创建一个脚本。
如果您尝试访问任何对象中的 URL,请尝试在代码中设置以下内容(取决于您尝试访问 URL 的方式,例如,这里我们使用 WebClient 对象来设置以下参数)
创建 WebClient 对象并设置以下内容:-
WebClient webClient = null;
System.setProperty("jsse.enableSNIExtension", "false");
webClient.getOptions().setUseInsecureSSL(true);
这解决了我的两个问题:
解决方案:
private RestTemplate getRestTemplate() {
try {
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory scsf = new SSLConnectionSocketFactory(
SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(),
NoopHostnameVerifier.INSTANCE);
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(scsf)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
} catch (Exception exception) {
return new RestTemplate();
}
}
当我使用 org.apache.http.* 中的方法发出 http 请求时,我遇到了同样的错误。从你的堆栈跟踪来看,我认为即使你也在使用相同的。
当我使用java.net.HttpURLConnection时,这个错误消失了,并且能够成功连接。
import java.net.HttpURLConnection;
public static HttpURLConnection connectToWeb(String uri) {
HttpURLConnection connection = null;
try {
URL url = new URL(uri);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
} catch (MalformedURLException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
return connection;
}
/*
* Inner class for Proxy SSL Socket Connection.
*/
static class MyConnectionSocketFactory extends SSLConnectionSocketFactory {
private String proxyHost = null;
private Integer proxyPort = null;
public MyConnectionSocketFactory(final SSLContext sslContext, String proxyHost, Integer proxyPort) {
super(sslContext, new NoopHostnameVerifier());
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
}
@Override
public Socket createSocket(final HttpContext context) throws IOException {
logger.debug("Create Socket:" + " ProxyHost: " + proxyHost + ", ProxyPort: " + proxyPort);
InetSocketAddress socksaddr = new InetSocketAddress(proxyHost,proxyPort);
Proxy proxy = new Proxy(Proxy.Type.SOCKS, socksaddr);
return new Socket(proxy);
}
}
else if (proxyType.equalsIgnoreCase("socks")) {
logger.debug("Proxy Type: " + proxyType);
TrustStrategy acceptingTrustStrategy = new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return true;
}
};
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory csf = new MyConnectionSocketFactory(sslContext, proxyHost, proxyPort);
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(csf)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
restTemplate = new RestTemplate(requestFactory);
return;
} else {