1. 概述

在本文中,我们将了解如何初始化和 配置 OkHttpClient 以信任自签名证书 。为此,我们将设置一个最小的启用 HTTPS 的 Spring Boot 应用程序,并通过自签名证书进行保护。

有关该库的更多详细信息,请参阅我们关于 OkHttp 的文章集

2. 基本原理

在我们深入研究负责完成这项工作的代码之前,让我们先了解一下底线。 SSL 的本质是在任意两方(通常是客户端和服务器)之间建立安全连接 。此外,它还有助于 保护通过网络传输的数据的隐私和完整性

JSSE API是 Java SE 的安全组件,为 SSL/TLS 协议提供完整的 API 支持。

SSL 证书(又名数字证书)在建立 TLS 握手、促进通信双方之间的加密和信任方面发挥着至关重要的作用。自签名 SSL 证书不是由知名且受信任的证书颁发机构 (CA) 颁发的。开发人员可以轻松生成并签署它们,以便为其软件启用 HTTPS。

由于自签名证书不可信,浏览器和标准 HTTPS 客户端(例如OkHttpApache HTTP Client)默认情况下都不信任它们

最后,我们可以使用网络浏览器或 OpenSSL 命令行实用程序方便地获取服务器证书

3. 设置测试环境

为了演示使用 OkHttp 接受和信任自签名证书的应用程序,让我们快速配置并启动一个启用了 HTTPS 的简单 Spring Boot 应用程序(由自签名证书保护)。

默认配置将启动 Tomcat 服务器侦听端口 8443 并公开可通过 https://localhost:8443/welcome” 访问的安全 REST API。

现在,让我们使用 OkHttp 客户端向该服务器发出 HTTPS 请求并使用 “/welcome” API。

4.OkHttpClient 和SSL

本节将初始化一个 OkHttpClient 并使用它来连接到我们刚刚设置的测试环境。此外,我们将检查路径中遇到的错误,并逐步实现使用 OkHttp 信任自签名证书的最终目标。

首先,让我们为 OkHttpClient 创建一个构建器:

OkHttpClient.Builder builder = new OkHttpClient.Builder();

另外,让我们声明我们将在本教程中使用的 HTTPS URL:

int SSL_APPLICATION_PORT = 8443;
String HTTPS_WELCOME_URL = "https://localhost:" + SSL_APPLICATION_PORT + "/welcome";

4.1. SSLHandshakeException

如果没有为 SSL 配置 OkHttpClient ,如果我们尝试使用 HTTPS URL,我们会收到安全异常:

@Test(expected = SSLHandshakeException.class)
public void whenHTTPSSelfSignedCertGET_thenException() {
    builder.build()
    .newCall(new Request.Builder()
    .url(HTTPS_WELCOME_URL).build())
    .execute();
}

堆栈跟踪是:

javax.net.ssl.SSLHandshakeException: PKIX path building failed: 
    sun.security.provider.certpath.SunCertPathBuilderException:
    unable to find valid certification path to requested target
    ...

上述错误恰恰意味着服务器使用了未经证书颁发机构 (CA) 签名的自签名证书。

因此,客户端无法验证直到根证书的信任链,因此抛出 SSLHandshakeException

4.2. SSLPeerUnverifiedException

现在,让我们配置信任证书的 OkHttpClient ,无论其性质如何 - CA 签名还是自签名。

首先,我们需要创建自己的 TrustManager ,它使默认证书验证无效并使用我们的自定义实现覆盖这些验证:

TrustManager TRUST_ALL_CERTS = new X509TrustManager() {
    @Override
    public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
    }

    @Override 
    public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
    }

    @Override
    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
        return new java.security.cert.X509Certificate[] {};
    }
};

接下来,我们将使用上面的 TrustManager 来初始化 SSLContext ,并设置 OkHttpClient 构建器的 SSLSocketFactory

SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[] { TRUST_ALL_CERTS }, new java.security.SecureRandom());
builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) TRUST_ALL_CERTS);

再次,让我们运行测试。不难相信,即使经过上述调整,使用 HTTPS URL 也会引发错误:

@Test(expected = SSLPeerUnverifiedException.class)
public void givenTrustAllCerts_whenHTTPSSelfSignedCertGET_thenException() {
    // initializing the SSLContext and set the sslSocketFactory
    builder.build()
        .newCall(new Request.Builder()
        .url(HTTPS_WELCOME_URL).build())
        .execute();
}

确切的错误是:

javax.net.ssl.SSLPeerUnverifiedException: Hostname localhost not verified:
    certificate: sha256/bzdWeeiDwIVjErFX98l+ogWy9OFfBJsTRWZLB/bBxbw=
    DN: CN=localhost, OU=localhost, O=localhost, L=localhost, ST=localhost, C=IN
    subjectAltNames: []

这是由于一个众所周知的问题——主机名验证失败。大多数 HTTP 库都会根据证书的 subjectAlternativeName 的 DNS 名称字段执行主机名验证 ,这在服务器的自签名证书中不可用,如上面的详细堆栈跟踪所示。

4.3.覆盖 主机名验证器

正确配置 OkHttpClient 的最后一步是禁用默认的 HostnameVerifier 并将其替换为另一个绕过主机名验证的 HostnameVerifier。

让我们添加最后一块定制内容:

builder.hostnameVerifier(new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
});

现在,让我们最后一次运行测试:

@Test
public void givenTrustAllCertsSkipHostnameVerification_whenHTTPSSelfSignedCertGET_then200OK() {
    // initializing the SSLContext and set the sslSocketFactory
    // set the custom hostnameVerifier
    Response response = builder.build()
        .newCall(new Request.Builder()
        .url(HTTPS_WELCOME_URL).build())
        .execute();
    assertEquals(200, response.code());
    assertNotNull(response.body());
    assertEquals("<h1>Welcome to Secured Site</h1>", response.body()
        .string());
}

最后, OkHttpClient 成功地能够使用由自签名证书保护的 HTTPS URL

5. 结论

在本教程中,我们了解了如何为 OkHttpClient 配置 SSL,以便它能够信任自签名证书并使用任何 HTTPS URL。

然而,需要考虑的重要一点是,虽然这种设计完全省略了证书验证和主机名验证,但客户端和服务器之间的所有通信仍然是加密的。两方之间的信任会丢失,但 SSL 握手和加密不会受到损害。

与往常一样,我们可以在 GitHub 上找到完整的源代码。