Android使用OkHttp请求自签名的https网站

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android使用OkHttp请求自签名的https网站相关的知识,希望对你有一定的参考价值。

参考技术A

很多公司考虑到安全问题,项目中都采用https加密协议进行数据传输。但是一些公司又不想花一笔钱去CA申请证书,所以就采用自签名的证书。

OkHttp默认是可以访问通过CA认证的HTTPS链接,例如百度首页也是https链接( https://www.baidu.com/ )。 但是如果是你们公司自签名(即自己用keytool生成的证书,而不是采用通过CA认证的证书)的服务器,OkHttp是无法访问的,例如访问12306网站( https://kyfw.12306.cn/otn/ ) ,会报如下错误:

HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。握手过程的简单描述如下:

握手过程中如果有任何错误,都会使加密连接断开,从而阻止了隐私信息的传输。

以下我们使用12306网站为例

注意:别忘了加权限和依赖okhttp库

Demo地址: https://github.com/wildma/okhttps
参考博客: http://blog.csdn.Net/lmj623565791/article/details/48129405

如何使用 OkHttp/Retrofit 重试 HTTP 请求?

【中文标题】如何使用 OkHttp/Retrofit 重试 HTTP 请求?【英文标题】:How to retry HTTP requests with OkHttp/Retrofit? 【发布时间】:2014-08-25 02:37:39 【问题描述】:

我在我的 Android 项目中使用 Retrofit/OkHttp (1.6)。

我没有发现任何一个内置的请求重试机制。在搜索更多时,我读到 OkHttp 似乎有静默重试。我没有看到在我的任何连接(HTTP 或 HTTPS)上发生这种情况。如何使用 okclient 配置重试?

目前,我正在捕获异常并重试维护计数器变量。

【问题讨论】:

@JesseWilson:我发现重试对于较慢的网络比较长的连接超时更有用。你不这么认为吗? 有时 api 会有一个响应代码,指示需要发出另一个请求(重新启动身份验证令牌、会话令牌或 XYZ 令牌),然后重试原始请求。这在 Volley 中很容易实现。我很想改用改造,但我看不到以通用方式完成这种管道的方法。 您是否找到了比捕获响应异常更好的方法,@SlowAndSteady?我目前正在更大规模地实现这一点,并认为我的类似方法应该进行重构。谢谢。 @JoshPinter :抱歉,找不到其他内容。我不确定 OhHttp 2.0 是否添加了对此的支持 - 你可能想看看。 @SlowAndSteady 好的,太好了,感谢您的更新。作为记录,我决定使用类似于此处概述的模式:***.com/a/8658067/293280 【参考方案1】:

对于 Retrofit 2.x;

您可以使用Call.clone() 方法克隆请求并执行它。

对于改造 1.x;

您可以使用Interceptors。创建自定义拦截器

    OkHttpClient client = new OkHttpClient();
    client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
    client.interceptors().add(new Interceptor() 
        @Override
        public Response intercept(Chain chain) throws IOException 
            Request request = chain.request();

            // try the request
            Response response = chain.proceed(request);

            int tryCount = 0;
            while (!response.isSuccessful() && tryCount < 3) 

                Log.d("intercept", "Request is not successful - " + tryCount);

                tryCount++;

                // retry the request
                response = chain.proceed(request);
            

            // otherwise just pass the original response on
            return response;
        
    );

并在创建 RestAdapter 时使用它。

new RestAdapter.Builder()
        .setEndpoint(API_URL)
        .setRequestInterceptor(requestInterceptor)
        .setClient(new OkClient(client))
        .build()
        .create(Adapter.class);

【讨论】:

连接超时,响应为空。有了这个,我们如何检查 response.isSuccessful() ? 但是有没有办法计算重试次数?克隆只是复制调用,以便它可以再次执行,但它不计算在内。 当没有网络连接时,Response response = chain.proceed(request);之后的所有代码都将无法访问,因为不会收到Response 在调用重试之前需要做response.close() @malhobayyeb response = chain.call().clone().execute();【参考方案2】:

我不知道这是否适合您,但您可以将 RxJava 与 Retrofit 一起使用。

Retrofit 能够在休息调用时返回 Observables。在 Observable 上,您只需调用 retry(count) 即可在 Observable 发出错误时重新订阅它。

您必须像这样在界面中定义调用:

@GET("/data.json")
Observable<DataResponse> fetchSomeData();

然后你可以像这样订阅这个 Observable:

restApi.fetchSomeData()
.retry(5)  // Retry the call 5 times if it errors
.subscribeOn(Schedulers.io())  // execute the call asynchronously
.observeOn(AndroidSchedulers.mainThread())  // handle the results in the ui thread
.subscribe(onComplete, onError); 
// onComplete and onError are of type Action1<DataResponse>, Action1<Throwable>
// Here you can define what to do with the results

我遇到了和你一样的问题,这实际上是我的解决方案。 RxJava 是一个非常好的库,可以与 Retrofit 结合使用。除了重试之外,您甚至可以做很多很酷的事情(例如composing and chaining calls)。

【讨论】:

你试过这个吗?似乎在 Retrofit Observable 上调用 retry() (或只是再次订阅)实际上并没有再次执行请求。 @pocmo 请看我的回复,也许会有所帮助【参考方案3】:

我认为您不应该将 API 处理(由改造/okhttp 完成)与重试混合使用。重试机制更加正交,也可以在许多其他情况下使用。所以我使用 Retrofit/OkHTTP 来处理所有的 API 调用和请求/响应处理,并在上面引入另一个层,用于重试 API 调用。

到目前为止,在我有限的 Java 经验中,我发现 jhlaterman 的 Failsafe 库(github:jhalterman/failsafe)是一个非常通用的库,可以干净地处理许多“重试”情况。举个例子,下面是我如何将它与一个改造实例化的 mySimpleService 一起使用,以进行身份​​验证 -

AuthenticationResponse authResp = Failsafe.with(
new RetryPolicy().retryOn(Arrays.asList(IOException.class, AssertionError.class))
        .withBackoff(30, 500, TimeUnit.MILLISECONDS)
        .withMaxRetries(3))
.onRetry((error) -> logger.warn("Retrying after error: " + error.getMessage()))
.get(() -> 
    AuthenticationResponse r = mySimpleAPIService.authenticate(
            new AuthenticationRequest(username,password))
            .execute()
            .body();

    assert r != null;

    return r;
);

上面的代码捕获套接字异常、连接错误、断言失败,并在它们上重试最多 3 次,具有指数退避。它还允许您自定义重试行为,并允许您指定回退。它的可配置性很强,可以适应大多数重试情况。

请随意查看该库的文档,因为除了重试之外,它还提供了许多其他好处。

【讨论】:

从设计角度指出 API 调用 + 处理与重试调用的正交性质 - 一个比另一个位于更高级别。 这很酷,但当前版本 2.3.1 似乎需要 API 26,因为使用的时间单位 (ChronoUnit) @behelit 还有什么问题?当您撰写评论时,API 26 已经超过 2 年(2017 年 8 月发布)。我认识的大多数严肃的 Android 开发人员都通过 Google Play 分发他们的应用程序。如果您想使用 Play,您必须使 SDK/API 级别保持最新……Google 从 2018 年 8 月开始强制您使用 API 26 来开发新应用,并从 2018 年 11 月开始强制您使用 API 26 来更新现有应用。【参考方案4】:

response.isSuccessful() 的问题是当您遇到像 SocketTimeoutException 这样的异常时。

我修改了原来的代码来修复它。

OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
client.interceptors().add(new Interceptor() 
    @Override
    public Response intercept(Chain chain) throws IOException 
        Request request = chain.request();
        Response response = null;
        boolean responseOK = false;
        int tryCount = 0;

        while (!responseOK && tryCount < 3) 
            try 
                 response = chain.proceed(request);
                 responseOK = response.isSuccessful();                  
            catch (Exception e)
                 Log.d("intercept", "Request is not successful - " + tryCount);                     
            finally
                 tryCount++;      
            
        

        // otherwise just pass the original response on
        return response;
    
);

希望对您有所帮助。 问候。

【讨论】:

但是即使服务器宕机或其他情况也会重试 当网络不可用时它会崩溃。我正在尝试通过 SSL 请求,添加自定义标头并添加了另一个日志拦截器。 如果我们有超时或没有连接,它返回null,然后它生成NullPointerException 要修复nullPointerException(当没有互联网连接时,如上所述)需要用return response != null ? response : chain.proceed(request);替换最后的return response; @Tilman Hausherr OkHttpClient.Builder().readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS)【参考方案5】:

感谢最佳答案,这对我有用。如果出现连接问题,最好等待几秒钟再重试。

public class ErrorInterceptor implements Interceptor 
ICacheManager cacheManager;
Response response = null;
int tryCount = 0;
int maxLimit = 3;
int waitThreshold = 5000;
@Inject
public ErrorInterceptor() 



@Override
public Response intercept(Chain chain)

   // String language =  cacheManager.readPreference(PreferenceKeys.LANGUAGE_CODE);
  Request request = chain.request();
  response =  sendReqeust(chain,request);
    while (response ==null && tryCount < maxLimit) 
        Log.d("intercept", "Request failed - " + tryCount);
        tryCount++;
        try 
            Thread.sleep(waitThreshold); // force wait the network thread for 5 seconds
         catch (InterruptedException e) 
            e.printStackTrace();
        
       response = sendReqeust(chain,request);
    
    return response;


private Response sendReqeust(Chain chain, Request request)
    try 
        response = chain.proceed(request);
        if(!response.isSuccessful())
            return null;
        else
        return response;
     catch (IOException e) 
      return null;
    

【讨论】:

你能帮帮我吗?当服务器给出错误 500 或其他错误时,它不起作用 -->HTTP FAILED: java.lang.IllegalStateException: cannot make a new request because the previous response is still open【参考方案6】:

在 OkHttp 3.9.1 上对我有用的解决方案(考虑这个问题的其他答案):

@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException 
    Request  request      = chain.request();
    int      retriesCount = 0;
    Response response     = null;

    do 
        try 
            response = chain.proceed(request);

        // Retry if no internet connection.
         catch (ConnectException e) 
            Log.e(TAG, "intercept: ", e);
            retriesCount++;

            try 
                Thread.sleep(RETRY_TIME);

             catch (InterruptedException e1) 
                Log.e(TAG, "intercept: ", e1);
            
        

     while (response == null && retriesCount < MAX_RETRIES);

    // If there was no internet connection, then response will be null.
    // Need to initialize response anyway to avoid NullPointerException.
    if (response == null) 
        response = chain.proceed(newRequest);
    

    return response;

【讨论】:

【参考方案7】:

我发现思南Kozak提供的方式(OKHttpClient拦截器)在http连接失败时不起作用,目前还没有关于http响应的问题。

所以我使用另一种方法来挂钩 Observable 对象,在其上调用 .retryWhen。 另外,我添加了 retryCount 限制。

import retrofit2.Call;
import retrofit2.CallAdapter;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.HttpException;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.jackson.JacksonConverterFactory;
import rx.Observable;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

然后

    RxJavaCallAdapterFactory originCallAdaptorFactory = RxJavaCallAdapterFactory.create();

    CallAdapter.Factory newCallAdaptorFactory = new CallAdapter.Factory() 
        @Override
        public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) 

            CallAdapter<?> ca = originCallAdaptorFactory.get(returnType, annotations, retrofit);

            return new CallAdapter<Observable<?>>() 

                @Override
                public Type responseType() 
                    return ca.responseType();
                

                int restRetryCount = 3;

                @Override
                public <R> Observable<?> adapt(Call<R> call) 
                    Observable<?> rx = (Observable<?>) ca.adapt(call);

                    return rx.retryWhen(errors -> errors.flatMap(error -> 
                        boolean needRetry = false;
                        if (restRetryCount >= 1) 
                            if (error instanceof IOException) 
                                needRetry = true;
                             else if (error instanceof HttpException) 
                                if (((HttpException) error).code() != 200) 
                                    needRetry = true;
                                
                            
                        

                        if (needRetry) 
                            restRetryCount--;
                            return Observable.just(null);
                         else 
                            return Observable.error(error);
                        
                    ));
                
            ;
        
    ;                

然后 添加或替换

.addCallAdapterFactory(RxJavaCallAdapterFactory.create())

.addCallAdapterFactory(newCallAdaptorFactory)

例如:

return new Retrofit
        .Builder()
        .baseUrl(baseUrl)
        .client(okClient)
        .addCallAdapterFactory(newCallAdaptorFactory)
        .addConverterFactory(JacksonConverterFactory.create(objectMapper));

注意:为简单起见,我只是将HTTP代码> 404代码视为重试,请自行修改。

另外,如果http响应是200,那么上面的rx.retryWhen就不会被调用,如果你坚持要检查这样的响应,你可以在.retryWhen之前加上rx.subscribeOn(...throw error...

【讨论】:

【参考方案8】:

对于那些更喜欢拦截器来处理重试问题的人 - 基于思南的回答,这是我提出的拦截器,它包括重试计数和回退延迟,并且仅在网络可用且请求未被取消时重试尝试。 (仅处理 IOExceptions(SocketTimeout、UnknownHost 等))

    builder.addInterceptor(new Interceptor() 
        @Override
        public Response intercept(Chain chain) throws IOException 
            Request request = chain.request();

            // try the request
            Response response = null;
            int tryCount = 1;
            while (tryCount <= MAX_TRY_COUNT) 
                try 
                    response = chain.proceed(request);
                    break;
                 catch (Exception e) 
                    if (!NetworkUtils.isNetworkAvailable()) 
                        // if no internet, dont bother retrying request
                        throw e;
                    
                    if ("Canceled".equalsIgnoreCase(e.getMessage())) 
                        // Request canceled, do not retry
                        throw e;
                    
                    if (tryCount >= MAX_TRY_COUNT) 
                        // max retry count reached, giving up
                        throw e;
                    

                    try 
                        // sleep delay * try count (e.g. 1st retry after 3000ms, 2nd after 6000ms, etc.)
                        Thread.sleep(RETRY_BACKOFF_DELAY * tryCount);
                     catch (InterruptedException e1) 
                        throw new RuntimeException(e1);
                    
                    tryCount++;
                
            

            // otherwise just pass the original response on
            return response;
        
    );

【讨论】:

【参考方案9】:

只想分享我的版本。它使用 rxJava retryWhen 方法。我的版本每 N=15 秒重试一次连接,并且在互联网连接恢复时几乎立即发出重试。

public class RetryWithDelayOrInternet implements Function<Flowable<? extends Throwable>, Flowable<?>> 
public static boolean isInternetUp;
private int retryCount;

@Override
public Flowable<?> apply(final Flowable<? extends Throwable> attempts) 
    return Flowable.fromPublisher(s -> 
        while (true) 
            retryCount++;
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                attempts.subscribe(s);
                break;
            
            if (isInternetUp || retryCount == 15) 
                retryCount = 0;
                s.onNext(new Object());
            
        
    )
            .subscribeOn(Schedulers.single());

你应该在 .subscribe 之前使用它,如下所示:

.retryWhen(new RetryWithDelayOrInternet())

您应该手动更改 isInternetUp 字段

public class InternetConnectionReceiver extends BroadcastReceiver 


@Override
public void onReceive(Context context, Intent intent) 
    boolean networkAvailable = isNetworkAvailable(context);
    RetryWithDelayOrInternet.isInternetUp = networkAvailable;

public static boolean isNetworkAvailable(Context context) 
    ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
    return activeNetworkInfo != null && activeNetworkInfo.isConnected();

【讨论】:

【参考方案10】:

它似乎将出现在 API 规范的改造 2.0 中: https://github.com/square/retrofit/issues/297。 目前,最好的方法似乎是捕获异常并手动重试。

【讨论】:

【参考方案11】:

如docs 中所述,更好的方法可能是使用烘焙的身份验证器,例如: 私有最终 OkHttpClient 客户端 = 新 OkHttpClient();

  public void run() throws Exception 
    client.setAuthenticator(new Authenticator() 
      @Override public Request authenticate(Proxy proxy, Response response) 
        System.out.println("Authenticating for response: " + response);
        System.out.println("Challenges: " + response.challenges());
        String credential = Credentials.basic("jesse", "password1");
        return response.request().newBuilder()
            .header("Authorization", credential)
            .build();
      

      @Override public Request authenticateProxy(Proxy proxy, Response response) 
        return null; // Null indicates no attempt to authenticate.
      
    );

    Request request = new Request.Builder()
        .url("http://publicobject.com/secrets/hellosecret.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  

【讨论】:

【参考方案12】:

我在这个问题上做了很多尝试,试图找到重试改造请求的最佳方法。我正在使用 Retrofit 2,所以我的解决方案适用于 Retrofit 2。对于 Retrofit 1,您必须像此处接受的答案一样使用拦截器。 @joluet 的答案是正确的,但他没有提到需要在 .subscribe(onComplete, onError) 方法之前调用 retry 方法。这非常重要,否则不会像@joluet 回答中提到的@pocmo 那样再次重试请求。这是我的例子:

final Observable<List<NewsDatum>> newsDetailsObservable = apiService.getCandidateNewsItem(newsId).map((newsDetailsParseObject) -> 
                    return newsDetailsParseObject;
                );

newsDetailsObservable.subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .retry((integer, throwable) -> 
                //MAX_NUMBER_TRY is your maximum try number
                if(integer <= MAX_NUMBER_TRY)
                    return true;//this will retry the observable (request)
                
                return false;//this will not retry and it will go inside onError method
            )
            .subscribe(new Subscriber<List<NewsDatum>>() 
                @Override
                public void onCompleted() 
                    // do nothing
                

                @Override
                public void onError(Throwable e) 
                   //do something with the error
                

                @Override
                public void onNext(List<NewsDatum> apiNewsDatum) 
                    //do something with the parsed data
                
            );

apiService 是我的 RetrofitServiceProvider 对象。

顺便说一句:我使用的是 Java 8,所以代码中有很多 lambda 表达式。

【讨论】:

我已经这样做了,根据我的日志,retrofitl 只调用网络一次。这个方法有问题!【参考方案13】:

有效的产品解决方案。

public int callAPI() 
    return 1; //some method to be retried


public int retrylogic()  throws InterruptedException, IOException
    int retry = 0;
    int status = -1;
    boolean delay = false;
    do 
        if (delay) 
            Thread.sleep(2000);
        

        try 
            status = callAPI();
        
        catch (Exception e) 
            System.out.println("Error occured");
            status = -1;
        
        finally 
            switch (status) 
            case 200:
                System.out.println(" **OK**");
                return status; 
            default:
                System.out.println(" **unknown response code**.");
                break;
            
            retry++;
            System.out.println("Failed retry " + retry + "/" + 3);
            delay = true;

         
    while (retry < 3);

    System.out.println("Aborting download of dataset.");
    return status;

【讨论】:

【参考方案14】:

正如之前的用户所说,如果您使用的是 Retrofit2,那么 call.clone 就足够了,但我还想添加一个简单的示例来说明它的外观:

public class CallbackImpl implements Callback<ResponseBody> 
    private final Set<Integer> retryCode = new HashSet<>(Arrays.asList(503, 504));
    int requestRetry  = 1;

    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) 
        if (response.code() == 201) 
            // Object was created.
         else 
            if (requestRetry != 0 && retryCode.contains(response.code())) 
                call.clone().enqueue(this);
             else 
                // Handle the error
            
        
    

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable throwable) 
        if (throwable instanceof IOException) 
            // Network failure
         else 
            // Conversion Issue
        
    

【讨论】:

【参考方案15】:

Failsafe 3.2.2 有一个OkHttp module,这让这很容易:

Call call = client.newCall(request);
RetryPolicy<Response> retryPolicy = RetryPolicy.ofDefaults();
FailsafeCall failsafeCall = FailsafeCall.with(retryPolicy).compose(call);

// Execute with retries
Response response = failsafeCall.execute();

Retrofit 也有类似的支持。 RetryPolicy 支持各种配置。这适用于同步和异步执行并支持取消。

【讨论】:

以上是关于Android使用OkHttp请求自签名的https网站的主要内容,如果未能解决你的问题,请参考以下文章

Android笔记-使用okhttp3库发送http请求

Android开发 - Retrofit 2 使用自签名的HTTPS证书进行API请求

Android:OkHttp的理解和使用

证书固定不适用于 Android 上的 OkHttp

Android网络请求库【OkHttp4.9.3】基本用法与原理分析

我可以通过 NPN 协商使用 OkHttp 发送 http/2 请求吗?