从 Java 应用程序调用 https://outlook.office365.com/EWS/Exchange.asmx 时出现随机 SSLHandshakeException
Posted
技术标签:
【中文标题】从 Java 应用程序调用 https://outlook.office365.com/EWS/Exchange.asmx 时出现随机 SSLHandshakeException【英文标题】:random SSLHandshakeException when calling https://outlook.office365.com/EWS/Exchange.asmx from Java application 【发布时间】:2021-10-10 13:03:29 【问题描述】:我有一个小型应用程序 Java 11,它在容器中运行并在 https://outlook.office365.com/EWS/Exchange.asmx 上在线连接到 Exchange。我正在使用 OKHttp3 (v4.9.1) 连接到它。
大多数时候,它可以工作.. 但有时,它会失败,并出现以下异常:
引起:javax.net.ssl.SSLHandshakeException:远程主机终止 在握手 java.base/sun.security.ssl.SSLSocketImpl.handleEOF(SSLSocketImpl.java:1321) 在 java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1160) 在 java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1063) 在 java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:402) 在 okhttp3.internal.connection.RealConnection.connectTls(RealConnection.kt:379) 在 okhttp3.internal.connection.RealConnection.establishProtocol(RealConnection.kt:337) 在 okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:209) 在 okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.kt:226) 在 okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.kt:106) 在 okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.kt:74) 在 okhttp3.internal.connection.RealCall.initExchange$okhttp(RealCall.kt:255) 在 okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:32) 在 okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) 在 okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95) 在 okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) 在 okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83) 在 okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) 在 okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76) 在 okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) 在 okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.kt:221) 在 okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) 在 okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201) 在 okhttp3.internal.connection.RealCall.execute(RealCall.kt:154) 在 vincent.email.kpi.tracker.o365.MyHttpClient.send(MyHttpClient.java:47) 在 com.microsoft.aad.msal4j.HttpHelper.executeHttpRequestWithRetries(HttpHelper.java:86) 在 com.microsoft.aad.msal4j.HttpHelper.executeHttpRequest(HttpHelper.java:64) ...省略了 7 个常见帧 原因:java.io.EOFException: SSL peer 错误地关闭 java.base/sun.security.ssl.SSLSocketInputRecord.decode(SSLSocketInputRecord.java:167) 在 java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:108) 在 java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1152) ... 省略了 31 个常用框架
它随机失败的事实让我感到困惑......它在同一个 Docker 镜像中运行,所以我这边的配置和证书不会从一个运行到另一个运行。我希望它在 office365 服务器上是一样的。
我可以在某处添加任何内容以使其更具“确定性”吗?
下面是我的代码:
import com.microsoft.aad.msal4j.HttpMethod;
import com.microsoft.aad.msal4j.HttpRequest;
import com.microsoft.aad.msal4j.HttpResponse;
import com.microsoft.aad.msal4j.IHttpResponse;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.Route;
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level;
public class MyHttpClient implements com.microsoft.aad.msal4j.IHttpClient
public static final String PROXY_HOST = "my.proxy.url";
public static final int PROXY_PORT = 8080;
private final OkHttpClient client;
public MyHttpClient(String authUser, String authPassword)
this.client = buildHttpClient(authUser,authPassword);
@Override
public IHttpResponse send(HttpRequest httpRequest) throws IOException
// Map URL, headers, and body from MSAL's HttpRequest to OkHttpClient request object
var request = buildOkRequestFromMsalRequest(httpRequest);
// Execute Http request with OkHttpClient
var okHttpResponse= client.newCall(request).execute();
// Map status code, headers, and response body from OkHttpClient's Response object to MSAL's IHttpResponse
return buildMsalResponseFromOkResponse(okHttpResponse);
private static IHttpResponse buildMsalResponseFromOkResponse(Response okHttpResponse) throws IOException
var msal4j = new HttpResponse();
msal4j.statusCode(okHttpResponse.code());
for (String headerKey : okHttpResponse.headers().names())
List<String> val = okHttpResponse.headers(headerKey);
msal4j.headers().put(headerKey, val);
msal4j.body(okHttpResponse.body().string());
return msal4j;
private static Request buildOkRequestFromMsalRequest(HttpRequest httpRequest)
var builder=new Request.Builder()
.url(httpRequest.url());
if(httpRequest.httpMethod()== HttpMethod.POST)
builder.method("POST", RequestBody.create(httpRequest.body().getBytes(StandardCharsets.UTF_8)));
//defaults to GET, with no body
if(httpRequest.headers()!=null)
builder.headers(Headers.of(httpRequest.headers()));
return builder.build();
private static OkHttpClient buildHttpClient(final String authUser, final String authPassword)
okhttp3.Authenticator proxyAuthenticator = new okhttp3.Authenticator()
@Override
public Request authenticate(Route route, Response response) throws IOException
String credential = Credentials.basic(authUser, authPassword);
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
;
var logging = new HttpLoggingInterceptor();
logging.setLevel(Level.BODY);
return new OkHttpClient.Builder()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_HOST, PROXY_PORT)))
.proxyAuthenticator(proxyAuthenticator)
//TODO should no ignore certificates in prod..
// --> certificate should be added in store
.sslSocketFactory(trustAllSslSocketFactory, (X509TrustManager)trustAllCerts[0])
.addInterceptor(logging)
.hostnameVerifier((hostname, session) -> true)
.build();
private static final TrustManager[] trustAllCerts = new TrustManager[]
new X509TrustManager()
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType)
throws CertificateException
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType)
throws CertificateException
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers()
return new java.security.cert.X509Certificate[];
;
private static final SSLContext trustAllSslContext;
static
try
trustAllSslContext = SSLContext.getInstance("SSL");
trustAllSslContext.init(null, trustAllCerts, new java.security.SecureRandom());
catch (NoSuchAlgorithmException | KeyManagementException e)
throw new Office365Exception(e);
private static final SSLSocketFactory trustAllSslSocketFactory = trustAllSslContext.getSocketFactory();
【问题讨论】:
【参考方案1】:我希望在 office365 服务器上也是如此。
我预计会有数千个 Office365 服务器实例在它们前面带有负载平衡器(等等)。每次您的客户端尝试连接时,它都可能与不同的 SSL 端点进行通信。它们可能并非都配置相同。这将是不确定性的一种可能解释。
我可以在某处添加任何内容以使其更具“确定性”吗?
可能不会。
但是您可以(应该)做的就是尝试找出远程服务器重置连接的原因。
在 SSL 连接握手期间服务器重置连接通常意味着 SSL 端点已决定无法建立安全连接。常见原因包括无法就要使用的协议版本或加密算法达成一致。 (这通常发生在客户端或服务器端使用不再被认为是安全的 SSL 协议或加密算法时。)
诊断问题的标准方法是使用-Djavax.net.debug=all
选项运行JVM。这将记录大量信息,包括 SSL 协商的详细信息。您可以比较客户端和服务器“提供”的内容,然后找出不匹配的内容。
更多信息;见Debugging SSL/TLS Connections。
【讨论】:
以上是关于从 Java 应用程序调用 https://outlook.office365.com/EWS/Exchange.asmx 时出现随机 SSLHandshakeException的主要内容,如果未能解决你的问题,请参考以下文章