从 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的主要内容,如果未能解决你的问题,请参考以下文章

从 Python 调用带有“子进程”的 Java 应用程序并读取 Java 应用程序输出

Java程序从main调用静态方法[关闭]

从 JavaScript 调用 Java,反之亦然,为啥?

从 Java 应用程序调用智能合约函数,无需监听事件

如何从 Java 和 JPA 调用存储过程

从 Java GUI 应用程序调用 servlet?