OkHttp是否支持接受自签名SSL证书?


75

我正在为拥有带有自签名SSL证书的服务器的客户工作。

我使用包装的OkHttp客户端使用Retrofit + CustomClient:

RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(Config.BASE_URL + Config.API_VERSION)
    .setClient(new CustomClient(new OkClient(), context))
    .build();

OkHttp默认情况下是否支持调用自签名SSL证书服务器?

顺便说说。哪个客户端默认使用翻新?我以为是OkHttp,但是当我进一步研究时,我意识到我需要导入OkHttp依赖项


关于第二部分,如果类路径可用,则Retrofit将使用OkHttp。检defaultClient()Platform.java以获取更多详细信息。
哈桑·易卜拉欣2014年

请参阅:[如何使用自定义SSL证书设置Retrofit2](adiyatmubarak.wordpress.com/tag/…
Guido Mocha,

Answers:


91

是的,它确实。

Retrofit允许您设置自定义HTTP客户端,该客户端已根据需要进行配置。

至于自签名的SSL证书,这里有一个讨论。该链接包含一些代码示例,这些示例可将自签名的SLL添加到AndroidDefaultHttpClient并将该客户端加载到Retrofit。

如果需要OkHttpClient接受自签名SSL,则需要javax.net.ssl.SSLSocketFactory通过setSslSocketFactory(SSLSocketFactory sslSocketFactory)方法将其传递给自定义实例。

获取套接字工厂的最简单的方法是让一个从javax.net.ssl.SSLContext所讨论这里

这是配置OkHttpClient的示例:

OkHttpClient client = new OkHttpClient();
KeyStore keyStore = readKeyStore(); //your method to obtain KeyStore
SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "keystore_pass".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(),trustManagerFactory.getTrustManagers(), new SecureRandom());
client.setSslSocketFactory(sslContext.getSocketFactory());

okhttp3的更新代码(使用生成器):

    OkHttpClient client = new OkHttpClient.Builder()
            .sslSocketFactory(sslContext.getSocketFactory())
            .build();

client这里现在配置为从您使用证书KeyStore。但是KeyStore,即使系统默认情况下信任它,它也将仅信任您中的证书,而不信任其他任何证书。(如果您只有自签名证书,KeyStore 并且尝试通过HTTPS连接到Google主页,则会得到SSLHandshakeException)。

您可以KeyStore从文件中获取实例,如docs所示

KeyStore readKeyStore() {
    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());

    // get user password and file input stream
    char[] password = getPassword();

    java.io.FileInputStream fis = null;
    try {
        fis = new java.io.FileInputStream("keyStoreName");
        ks.load(fis, password);
    } finally {
        if (fis != null) {
            fis.close();
        }
    }
    return ks;
}

如果您使用的是Android,则可以将其放入res/raw文件夹中,并Context使用实例从实例中获取

fis = context.getResources().openRawResource(R.raw.your_keystore_filename);

关于如何创建密钥库,有一些讨论。例如这里


我从哪里获得SSLContext?
奥利弗·迪克森

要接受系统CA证书以及证书的自定义密钥库,请查看此答案
DreamOfMirrors

2
OkHttpBuilder中的方法“ sslSocketFactory(SSLSocketFactory)”现在已被弃用,我们应该使用“ sslSoocketFactory(SSLSocketFactory,X506TrustManager)”。这样可以防止应用程序进行一些反思。来源:square.github.io/okhttp/3.x/okhttp/okhttp3/…–
MHogge,

fis = context.getResources()。openRawResource(R.raw.your_keystore_filename); -java.lang.ClassCastException:android.content.res.AssetManager $ AssetInputStream无法转换为java.io.FileInputStream
Zon

无需FileInputStream,InputStream即可。
Zon

10

对于okhttp3.OkHttpClient版本com.squareup.okhttp3:okhttp:3.2.0,您必须使用以下代码:

import okhttp3.Call;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

......

OkHttpClient.Builder clientBuilder = client.newBuilder().readTimeout(LOGIN_TIMEOUT_SEC, TimeUnit.SECONDS);

            boolean allowUntrusted = true;

            if (  allowUntrusted) {
                Log.w(TAG,"**** Allow untrusted SSL connection ****");
                final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        X509Certificate[] cArrr = new X509Certificate[0];
                        return cArrr;
                    }

                    @Override
                    public void checkServerTrusted(final X509Certificate[] chain,
                                                   final String authType) throws CertificateException {
                    }

                    @Override
                    public void checkClientTrusted(final X509Certificate[] chain,
                                                   final String authType) throws CertificateException {
                    }
                }};

                SSLContext sslContext = SSLContext.getInstance("SSL");

                sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
                clientBuilder.sslSocketFactory(sslContext.getSocketFactory());

                HostnameVerifier hostnameVerifier = new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        Log.d(TAG, "Trust Host :" + hostname);
                        return true;
                    }
                };
                clientBuilder.hostnameVerifier( hostnameVerifier);
            }

            final Call call = clientBuilder.build().newCall(request);

8
投票无效,因为这将禁用主机名验证,从而在中间保护中删除了人员。
Bryan

1
此解决方案仅在没有有效SSL证书的情况下用于调试。不用于生产代码!
Gugelhupf

1
OkHttpClient.Builder clientBuilder.sslSocketFactory()已弃用,不再适用于Android 10。请不要再使用我的建议。
Gugelhupf

8

要注意的另一件事是,如果在设备上预安装了CA,则可以使用OKHttp进行常规的https调用,而无需特殊的ssl箍。关键是将网络安全配置添加到清单中。

我知道要执行此操作的关键是我遇到了以下异常。

找不到证书路径的信任锚。

这是Google撰写的有关如何进行配置的好文章。 https://developer.android.com/training/articles/security-config

这是我的network_security_config.xml的示例

<?xml version="1.0" encoding="UTF-8" ?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="user"/>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
</network-security-config>

虽然,这可能不是问题的直接答案,但这可以帮助我解决:javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
vijay

谢谢,确切的答案我一直在寻找很久了
开发人员来自Jokela

至少在Android模拟器API 21中不起作用。尝试了XML文件的许多变体。
CoolMind

我确定我遗漏了一些东西,但是附件文档指出默认情况下API 23及以下版本信任。默认情况下,安全连接(使用协议,如TLS和HTTPS),从所有的应用程序信任预装系统的CA和应用定位的Android 6.0(API等级23),也降低默认信任的用户添加的CA存储
GR特使

5

从我们的应用程序获取OkHttpClient 3.0实例的两种方法,这些实例可以从密钥库中识别您的自签名证书(使用Android项目“原始”资源文件夹中准备好的pkcs12证书文件):

private static OkHttpClient getSSLClient(Context context) throws
                              NoSuchAlgorithmException,
                              KeyStoreException,
                              KeyManagementException,
                              CertificateException,
                              IOException {

  OkHttpClient client;
  SSLContext sslContext;
  SSLSocketFactory sslSocketFactory;
  TrustManager[] trustManagers;
  TrustManagerFactory trustManagerFactory;
  X509TrustManager trustManager;

  trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
  trustManagerFactory.init(readKeyStore(context));
  trustManagers = trustManagerFactory.getTrustManagers();

  if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
    throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
  }

  trustManager = (X509TrustManager) trustManagers[0];

  sslContext = SSLContext.getInstance("TLS");

  sslContext.init(null, new TrustManager[]{trustManager}, null);

  sslSocketFactory = sslContext.getSocketFactory();

  client = new OkHttpClient.Builder()
      .sslSocketFactory(sslSocketFactory, trustManager)
      .build();
  return client;
}

/**
 * Get keys store. Key file should be encrypted with pkcs12 standard. It    can be done with standalone encrypting java applications like "keytool". File password is also required.
 *
 * @param context Activity or some other context.
 * @return Keys store.
 * @throws KeyStoreException
 * @throws CertificateException
 * @throws NoSuchAlgorithmException
 * @throws IOException
*/
private static KeyStore readKeyStore(Context context) throws
                          KeyStoreException,
                          CertificateException,
                          NoSuchAlgorithmException,
                          IOException {
  KeyStore keyStore;
  char[] PASSWORD = "12345678".toCharArray();
  ArrayList<InputStream> certificates;
  int certificateIndex;
  InputStream certificate;

  certificates = new ArrayList<>();
  certificates.add(context.getResources().openRawResource(R.raw.ssl_pkcs12));

keyStore = KeyStore.getInstance("pkcs12");

for (Certificate certificate : certificates) {
    try {
      keyStore.load(certificate, PASSWORD);
    } finally {
      if (certificate != null) {
        certificate.close();
      }
    }
  }
  return keyStore;
}

certificates.add(context.getResources().openRawResource(R.raw.ssl_pkcs12));此行显示错误,您能给出原因吗?有没有为什么这个类非活性类
拉杰·库马尔·

2

我遇到了同样的问题,并使用okhttp客户端对其进行了如下修复

1.)将certificate文件添加到src/main/res/raw/,其中包含以下内容:

-----BEGIN CERTIFICATE-----
...=
-----END CERTIFICATE-----

2.)实例化okHttpClient:

OkHttpClient client = new OkHttpClient.Builder()
                .sslSocketFactory(getSslContext(context).getSocketFactory())
                .build();

3.)这是使用的getSslContext(Context context)方法:

SSLContext getSslContext(Context context) throws Exception {
    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); // "BKS"
    ks.load(null, null);

    InputStream is = context.getResources().openRawResource(R.raw.certificate);
    String certificate = Converter.convertStreamToString(is);

    // generate input stream for certificate factory
    InputStream stream = IOUtils.toInputStream(certificate);

    // CertificateFactory
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    // certificate
    Certificate ca;
    try {
        ca = cf.generateCertificate(stream);
    } finally {
        is.close();
    }

    ks.setCertificateEntry("my-ca", ca);

    // TrustManagerFactory
    String algorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
    // Create a TrustManager that trusts the CAs in our KeyStore
    tmf.init(ks);

    // Create a SSLContext with the certificate that uses tmf (TrustManager)
    sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());

    return sslContext;
}

如果需要向SslContext添加多个证书,是解决方案。


1

使用Retrofit 1.9,我可以采用以下策略接受任何证书:使用风险自负!接受任何证书都是危险的,您应该了解后果。一些相关的零件来自org.apache.http.ssl,因此您可能需要在此处输入一些内容。

// ...

    Client httpClient = getHttpClient();

    RestAdapter adapter = new RestAdapter.Builder()
        .setClient(httpClient)
        // ... the rest of your builder setup
        .build();

// ...

private Client getHttpClient() {
    try {
        // Allow self-signed (and actually any) SSL certificate to be trusted in this context
        TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;

        SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
            .loadTrustMaterial(null, acceptingTrustStrategy)
            .build();

        sslContext.getSocketFactory();

        SSLSocketFactory sf = sslContext.getSocketFactory();

        OkHttpClient client = new OkHttpClient();
        client.setSslSocketFactory(sf);

        return new OkClient(client);
    } catch (Exception e) {
        throw new RuntimeException("Failed to create new HTTP client", e);
    }
}

0

我知道这篇文章已经很老了,我想与OkHttp的最新更新(3.12.1我写作时的版本)共享适用于我的解决方案。

首先,您需要获取KeyStore对象,然后将其添加到TrustManager中:

/**
 *  @param context The Android context to be used for retrieving the keystore from raw resource
 * @return the KeyStore read or null on error
 */
private static KeyStore readKeyStore(Context context) {

    char[] password = "keystore_password".toCharArray();

    // for non-android usage:
    // try(FileInputStream is = new FileInputStream(keystoreName)) {

    try(InputStream is = context.getResources().openRawResource(R.raw.keystore)) {
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(is, password);
        return ks;
    } catch (CertificateException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    }

    return null;
}

现在,您可以OkHttpClient在密钥库中使用自签名证书来构建:

/**
 * @param context The Android context used to obtain the KeyStore
 * @return the builded OkHttpClient or null on error
 */
public static OkHttpClient getOkHttpClient(Context context) {

    try {
        TrustManagerFactory trustManagerFactory = TrustManagerFactory
                .getInstance(TrustManagerFactory.getDefaultAlgorithm());

        trustManagerFactory.init(readKeyStore(context));

        X509TrustManager trustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[]{trustManager}, null);

        return new OkHttpClient.Builder()
                .hostnameVerifier((hostname, session) -> {
                    HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
                    /* Never return true without verifying the hostname, otherwise you will be vulnerable
                    to man in the middle attacks. */
                    return  hv.verify("your_hostname_here", session);
                })
                .sslSocketFactory(sslContext.getSocketFactory(), trustManager)
                .build();

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (KeyManagementException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }

    return null;
}

请记住,强烈建议不要始终返回true,hostnameVerifier以免在中间攻击时有人身危险。


-4

以下代码允许您创建可与Retrofit一起使用的OkHttp客户端。Mailmustdie的回答是“更好”,因为它更安全,但是下面的代码片段实现起来更快

import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.ResponseBody;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import okio.BufferedSink;
import retrofit.client.Header;
import retrofit.client.OkClient;
import retrofit.client.Request;
import retrofit.client.Response;
import retrofit.mime.TypedInput;
import retrofit.mime.TypedOutput;

import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class TrustingOkClient extends OkClient {

static final int CONNECT_TIMEOUT_MILLIS = 15 * 1000; // 15s
static final int READ_TIMEOUT_MILLIS = 20 * 1000; // 20s

private static OkHttpClient generateDefaultOkHttp() {
    OkHttpClient client = new OkHttpClient();
    client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);



    final TrustManager[] certs = new TrustManager[]{new X509TrustManager() {

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkServerTrusted(final X509Certificate[] chain,
                                       final String authType) throws CertificateException {
        }

        @Override
        public void checkClientTrusted(final X509Certificate[] chain,
                                       final String authType) throws CertificateException {
        }
    }};

    SSLContext ctx = null;
    try {
        ctx = SSLContext.getInstance("TLS");
        ctx.init(null, certs, new SecureRandom());
    } catch (final java.security.GeneralSecurityException ex) {
    }

    try {
        final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
            @Override
            public boolean verify(final String hostname,
                                  final SSLSession session) {
                return true;
            }
        };
        client.setHostnameVerifier(hostnameVerifier);
        client.setSslSocketFactory(ctx.getSocketFactory());
    } catch (final Exception e) {
    }
    return client;
}

private final OkHttpClient client;

public TrustingOkClient() {
    this(generateDefaultOkHttp());
}

public TrustingOkClient(OkHttpClient client) {
    if (client == null) throw new NullPointerException("client == null");
    this.client = client;
}

@Override public Response execute(Request request) throws IOException {
    return parseResponse(client.newCall(createRequest(request)).execute());
}

static com.squareup.okhttp.Request createRequest(Request request) {
    com.squareup.okhttp.Request.Builder builder = new com.squareup.okhttp.Request.Builder()
            .url(request.getUrl())
            .method(request.getMethod(), createRequestBody(request.getBody()));

    List<Header> headers = request.getHeaders();
    for (int i = 0, size = headers.size(); i < size; i++) {
        Header header = headers.get(i);
        String value = header.getValue();
        if (value == null) value = "";
        builder.addHeader(header.getName(), value);
    }

    return builder.build();
}

static Response parseResponse(com.squareup.okhttp.Response response) {
    return new Response(response.request().urlString(), response.code(), response.message(),
            createHeaders(response.headers()), createResponseBody(response.body()));
}

private static RequestBody createRequestBody(final TypedOutput body) {
    if (body == null) {
        return null;
    }
    final MediaType mediaType = MediaType.parse(body.mimeType());
    return new RequestBody() {
        @Override public MediaType contentType() {
            return mediaType;
        }

        @Override public void writeTo(BufferedSink sink) throws IOException {
            body.writeTo(sink.outputStream());
        }

        @Override public long contentLength() {
            return body.length();
        }
    };
}

private static TypedInput createResponseBody(final ResponseBody body) {
    try {
        if (body.contentLength() == 0) {
            return null;
        }
        return new TypedInput() {
            @Override public String mimeType() {
                MediaType mediaType = body.contentType();
                return mediaType == null ? null : mediaType.toString();
            }

            @Override public long length() {
                try {
                    return body.contentLength();
                } catch (Exception exception) {
                    System.out.println(exception.toString());
                }
                throw new Error("createResponseBody has invalid length for its response");
            }

            @Override public InputStream in() throws IOException {
                return body.byteStream();
            }
        };
    } catch (Exception exception) {
        System.out.println(exception.toString());
    }
    throw new Error("createResponseBody has invalid content length for its response");
}

private static List<Header> createHeaders(Headers headers) {
    int size = headers.size();
    List<Header> headerList = new ArrayList<Header>(size);
    for (int i = 0; i < size; i++) {
        headerList.add(new Header(headers.name(i), headers.value(i)));
    }
    return headerList;
}

}

8
我想指出的是,这不仅比接受的答案“不够安全”,而且是完全不安全的。
GreyBeardedGeek,2016年

1
该解决方案允许在HTTP上使用https,HTTP更为安全,但不能抵御中间攻击。它比根本不安全要好一些,并且允许开发中的应用程序连接到https服务,此服务以后会以50美元的价格获得适当的证书。比花费大量时间获得可自行签名的证书与Android客户端一起工作要好得多
Bruno Carrier

1
投票无效,因为这将禁用主机名验证,从而在中间保护中删除了人员。接受的答案不应花费很多时间来实施。
布赖恩
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.