OkHttp基础概念解释详解手机开发

最近在整理Android常用第三方框架相关的东西,说道Android的框架,无外乎就是Android开发中常见的网络、图片缓存、数据交互、优化、页面等框架,其中网络作为一个基础部分,我相信大家更多的是使用OkHttp,而在长连接中有Socket和webSocket等,今天给大家总结下OkHttp相关的内容,部分参考网络资源。

OkHttp简介

OkHttp作为时下Android最火的Http第三方库可以说被大多数的Android客户端程序所使用,Retrofit底层也是使用OkHttp,与Volley等网络请求框架相比,OkHttp具有如下的一些特点:

  • HTTP/2支持所有访问相同主机的请求共享一个套接字。也就是说支持Google的SPDY协议,如果 SPDY 不可用,则通过连接池来减少请求延时。
  • 连接池减少了请求延迟(如果HTTP/2不可用)。
  • 透明GZIP压缩减少了下载大小。
  • 响应缓存完全避免了重复请求的网络使用。
  • 当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址

OkHttp官网地址:http://square.github.io/okhttp/
OkHttp GitHub地址:https://github.com/square/okhttp

使用示例

OkHttp的使用也非常简单,支持Get、Post等多种请求方式,并且支持文件等的上传下载等多种功能,可以说现在你业务中能涉及到的情况,OkHttp都能解决。下面是一些简单的使用示例。

同步Get请求

private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    Request request = new Request.Builder() 
        .url("http://publicobject.com/helloworld.txt") 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
    Headers responseHeaders = response.headers(); 
    for (int i = 0; i < responseHeaders.size(); i++) { 
      System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); 
    } 
 
    System.out.println(response.body().string()); 
  }

不过需要注意的是,作用在响应主体上的string()方法对于小文档来说是方便和高效的,但是如果响应主体比较大(大于1MB),应避免使用string(),因为它会加载整个文档到内存中。

异步Get请求

异步使用enqueue进行请求,例如:

private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    Request request = new Request.Builder() 
        .url("http://publicobject.com/helloworld.txt") 
        .build(); 
 
    client.newCall(request).enqueue(new Callback() { 
      @Override public void onFailure(Call call, IOException e) { 
        e.printStackTrace(); 
      } 
 
      @Override public void onResponse(Call call, Response response) throws IOException { 
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
        Headers responseHeaders = response.headers(); 
        for (int i = 0, size = responseHeaders.size(); i < size; i++) { 
          System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); 
        } 
 
        System.out.println(response.body().string()); 
      } 
    }); 
  } 

设置Header

典型的HTTP头工作起来像一个Map< String, String >,每一个字段有一个值或没有值。但是有一些头允许多个值,像Guava的Multimap。

使用Request进行请求头信息的设置时,有些信息再次设置是不会被覆盖的,例如addHeader(name, value),使用addHeader(name, value)来添加一个头而不移除已经存在的头。

private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    Request request = new Request.Builder() 
        .url("https://api.github.com/repos/square/okhttp/issues") 
        .header("User-Agent", "OkHttp Headers.java") 
        .addHeader("Accept", "application/json; q=0.5") 
        .addHeader("Accept", "application/vnd.github.v3+json") 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
    System.out.println("Server: " + response.header("Server")); 
    System.out.println("Date: " + response.header("Date")); 
    System.out.println("Vary: " + response.headers("Vary")); 
  } 

上传字符串

使用HTTP POST来发送请求(比如文件)主体到服务器,因为整个请求主体同时存在内存中,应避免使用这个API上传大的文档大于1MB。如果是大文件,可以使用OKHttp的断点续传功能。

public static final MediaType MEDIA_TYPE_MARKDOWN 
      = MediaType.parse("text/x-markdown; charset=utf-8"); 
 
  private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    String postBody = "" 
        + "Releases/n" 
        + "--------/n" 
        + "/n" 
        + " * _1.0_ May 6, 2013/n" 
        + " * _1.1_ June 15, 2013/n" 
        + " * _1.2_ August 11, 2013/n"; 
 
    Request request = new Request.Builder() 
        .url("https://api.github.com/markdown/raw") 
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody)) 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
    System.out.println(response.body().string()); 
  } 

当然,OkHttp也支持以stream的形式来上传文件等请求主体。

public static final MediaType MEDIA_TYPE_MARKDOWN 
      = MediaType.parse("text/x-markdown; charset=utf-8"); 
 
  private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    RequestBody requestBody = new RequestBody() { 
      @Override public MediaType contentType() { 
        return MEDIA_TYPE_MARKDOWN; 
      } 
 
      @Override public void writeTo(BufferedSink sink) throws IOException { 
        sink.writeUtf8("Numbers/n"); 
        sink.writeUtf8("-------/n"); 
        for (int i = 2; i <= 997; i++) { 
          sink.writeUtf8(String.format(" * %s = %s/n", i, factor(i))); 
        } 
      } 
 
      private String factor(int n) { 
        for (int i = 2; i < n; i++) { 
          int x = n / i; 
          if (x * i == n) return factor(x) + " × " + i; 
        } 
        return Integer.toString(n); 
      } 
    }; 
 
    Request request = new Request.Builder() 
        .url("https://api.github.com/markdown/raw") 
        .post(requestBody) 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
    System.out.println(response.body().string()); 
  } 

文件上传

文件的上传相对简单,直接提供File的路径即可。

public static final MediaType MEDIA_TYPE_MARKDOWN 
      = MediaType.parse("text/x-markdown; charset=utf-8"); 
 
  private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    File file = new File("README.md"); 
 
    Request request = new Request.Builder() 
        .url("https://api.github.com/markdown/raw") 
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file)) 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
    System.out.println(response.body().string()); 
  }

上传表格参数

OkHtpp支持使用FormBody.Builder来构建一个工作起来像HTML< form >标签的请求主体。键值对会使用一个兼容HTML form的URL编码进行编码。

private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    RequestBody formBody = new FormBody.Builder() 
        .add("search", "Jurassic Park") 
        .build(); 
    Request request = new Request.Builder() 
        .url("https://en.wikipedia.org/w/index.php") 
        .post(formBody) 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
    System.out.println(response.body().string()); 
  } 

多部分请求

MultipartBody.Builder可以构造复杂的请求主体与HTML文件上传表单兼容。multipart请求主体的每部分本身就是一个请求主体,可以定义它自己的头。如果存在自己的头,那么这些头应该描述部分主体,例如它的Content-Disposition。Content-Length和Content-Type会在其可用时自动添加。

private static final String IMGUR_CLIENT_ID = "..."; 
  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png"); 
 
  private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image 
    RequestBody requestBody = new MultipartBody.Builder() 
        .setType(MultipartBody.FORM) 
        .addFormDataPart("title", "Square Logo") 
        .addFormDataPart("image", "logo-square.png", 
            RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png"))) 
        .build(); 
 
    Request request = new Request.Builder() 
        .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) 
        .url("https://api.imgur.com/3/image") 
        .post(requestBody) 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
    System.out.println(response.body().string()); 
  }

缓存响应设置

要设置缓存响应,你需要一个进行读取和写入的缓存目录,以及一个缓存大小的限制。缓存目录应该是私有的,且不被信任的应用不能够读取它的内容。让多个缓存同时访问相同的混存目录是错误的。大多数应用应该只调用一次new OkHttpClient(),配置它们的缓存,并在所有地方使用相同的实例。否则两个缓存实例会相互进行干涉。

同时OkHttp还支持对缓存的时间和大小进行设置。如添加像Cache-Control:max-stale=3600设置请求头缓存大小,使用Cache-Control:max-age=9600来配置响应缓存时间。

网络超时配置

网络部分可能是由于连接问题,服务器可用性问题或者其他原因造成网络请求超时。所以在使用时,可以根据实际情况进行网络的超时设置。

private final OkHttpClient client; 
 
  public ConfigureTimeouts() throws Exception { 
    client = new OkHttpClient.Builder() 
        .connectTimeout(10, TimeUnit.SECONDS) 
        .writeTimeout(10, TimeUnit.SECONDS) 
        .readTimeout(30, TimeUnit.SECONDS) 
        .build(); 
  } 
 
  public void run() throws Exception { 
    Request request = new Request.Builder() 
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    System.out.println("Response completed: " + response); 
  } 

取消请求

OkHttp支持取消网络请求,使用Call.cancel()来立即停止一个正在进行的调用。如果一个线程正在写请求或读响应,它会接收到一个IOException,同步和异步调用都可以取消。

private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); 
  private final OkHttpClient client = new OkHttpClient(); 
 
  public void run() throws Exception { 
    Request request = new Request.Builder() 
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. 
        .build(); 
 
    final long startNanos = System.nanoTime(); 
    final Call call = client.newCall(request); 
 
    // Schedule a job to cancel the call in 1 second. 
    executor.schedule(new Runnable() { 
      @Override public void run() { 
        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f); 
        call.cancel(); 
        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f); 
      } 
    }, 1, TimeUnit.SECONDS); 
 
    try { 
      System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f); 
      Response response = call.execute(); 
      System.out.printf("%.2f Call was expected to fail, but completed: %s%n", 
          (System.nanoTime() - startNanos) / 1e9f, response); 
    } catch (IOException e) { 
      System.out.printf("%.2f Call failed as expected: %s%n", 
          (System.nanoTime() - startNanos) / 1e9f, e); 
    } 
  }

认证请求

如果网络请求涉及到认证机制,OkHttp也提供了Authenticator来进行应用证书认证,Authenticator的实现应该构建一个包含缺失证书的新请求,如果没有证书可用,返回null来跳过重试。

使用Response.challenges()来获取所有认证挑战的模式和领域。当完成一个Basic挑战时,使用Credentials.basic(username,password)来编码请求头。涉及的示例如下:

private final OkHttpClient client; 
 
  public Authenticate() { 
    client = new OkHttpClient.Builder() 
        .authenticator(new Authenticator() { 
          @Override public Request authenticate(Route route, Response response) throws IOException { 
            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(); 
          } 
        }) 
        .build(); 
  } 
 
  public void run() throws Exception { 
    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()); 
  } 

OkHttp的Call

OkHttp支持重写,重定向,跟进和重试,OkHttp会使用Call来模化满足请求的任务,然而中间的请求和响应是必要的。OkHttp提供了两种方式的Call:

  • Synchronous:线程会阻塞直到响应可读;
  • Asynchronous:在一个线程中入队请求,当你的响应可读时在另外一个线程获取回调。

请求可以从任何线程取消,如果请求还没有执行完成,会使请求失败,请求失败会出现IOException异常错误。

OkHttp支持同步和异步方式请求,对于同步调用,使用的是自己的线程并对管理你同时创建多少请求负责。对于异步调用,Dispatcher实现了最大并发请求的策略,你可以设置每个服务器最大值(默认是5)和所有最大值(默认是64)。

OkHttp网络链接

在使用OkHttp进行请求的时候,我们只需要提供请求的url地址即可实现网络的访问,其实OkHttp在规划连接服务器的连接时提供了三种类型:URL,Address和Route。
下面就分别来说一下这三种链接的关系即使用场合。

URL

URL是HTTP和网络的最基本的联系方式,成为统一资源定位符,URL是一个抽象的概念。

  • 它们规定了调用可能是明文(http)或密文(https),但是没有规定应该使用哪个加密算法。也没有规定如何验证对等的证书(HostnameVerifier)或者哪个证书可被信任(SSLSocketFactory)。
  • 每一个URL确定一个特定路径,每个服务器包含很多的URL。

Addresses

在OkHttp中,Addresses规定了服务器和所有连接服务器需要的静态配置:端口号,HTTPS设置和优先网络协议(如HTTP/2或SPDY)。共享相同address的URLs也可能共享相同的下层TCP socket连接。
共享一个连接有巨大的性能好处:低延迟,高吞吐量(因为TCP启动慢)和节省电源。OkHttp使用ConnectionPool来自动复用HTTP/1.X连接和多路传输HTTP/2和SPDY连接。

在OkHttp中,address的一些字段来自URL(机制,主机名,端口),剩下的来自OkHttpClient。

Routes

Routes提供了真正连接到服务器所需要的动态信息,它会Routes明确的要尝试的IP地址以及代理服务器,以及什么版本的TLS来协商(针对HTTPS连接)。

对于一个地址有可能有很多路由,一个存在多个数据中心的网络服务器可能在它的DNS响应中产生多个IP地址。

OkHttp网络连接流程

当你使用OkHttp请求一个URL时,下面是它执行的流程:
1. 它使用URL和配置的OkHttpClient来创建一个address,这个address规定了如何连接到服务器。
2. OkHttp尝试使用这个address从连接池中获取一个连接。
3. 如果它没有在池中找到一个连接,它会选择一个route来尝试。这通常意味着创建一个DNS请求来获取服务器的IP地址。
4. 如果这是一个新route,它会通过构建一个直接的socket连接或一个TLS隧道或一个直接的TLS连接来进行连接。如果需要它会执行TLS握手。
5. 然后发送HTTP请求然后读取响应。

当连接出现问题时,OkHttp会选择另外一个route进行尝试。一旦接收到服务端的响应,连接就会返回到池中,这样它可以在之后的请求复用,连接空闲一段时间会从池中移除。

拦截器

看过OkHttp源码分析的同学对于拦截器肯定不会陌生,在OkHttp中拦截器是所有的网络请求的必经之地,拦截器主要有以下一些作用。

1、拦截器可以一次性对所有的请求和返回值进行修改;
2、拦截器可以一次性对请求的参数和返回的结果进行编码,比如统一设置为UTF-8;
3、拦截器可以对所有的请求做统一的日志记录,不需要在每个请求开始或者结束的位置都添加一个日志操作;
4、其他需要对请求和返回进行统一处理的需求….

下面是一个最简单的拦截器使用,用来打印OkHttp的请求和收到的响应。

class LoggingInterceptor implements Interceptor { 
  @Override public Response intercept(Interceptor.Chain chain) throws IOException { 
    Request request = chain.request(); 
 
    long t1 = System.nanoTime(); 
    logger.info(String.format("Sending request %s on %s%n%s", 
        request.url(), chain.connection(), request.headers())); 
 
    Response response = chain.proceed(request); 
 
    long t2 = System.nanoTime(); 
    logger.info(String.format("Received response for %s in %.1fms%n%s", 
        response.request().url(), (t2 - t1) / 1e6d, response.headers())); 
 
    return response; 
  } 
} 

OkHttp使用列表来跟踪拦截器,并且拦截器按顺序被调用。栖拦截的模型如下:
这里写图片描述

OkHttp中的拦截器分为两类:APP层面的拦截器(Application Interception)、网络请求层面的拦截器(Network Interception)。在OkHttp中,首先从App Interceptor开始,然后执行Network Interceptor,最后又回到App Interceptor。

应用拦截器

下面我们使用OkHttpCleint.Builder上调用addInterceptor()来注册一个应用拦截器。代码如下:

OkHttpClient client = new OkHttpClient.Builder() 
    .addInterceptor(new LoggingInterceptor()) 
    .build(); 
 
Request request = new Request.Builder() 
    .url("http://www.publicobject.com/helloworld.txt") 
    .header("User-Agent", "OkHttp Example") 
    .build(); 
 
Response response = client.newCall(request).execute(); 
response.body().close();

如果我们需要将http://www.publicobject.com/helloworld.txt这个URL重定向到https://publicobject.com/helloworld.txt,那么OkHttp会自动跟进这个重定向。下面是重定向的相关的执行信息:

INFO: Sending request http://www.publicobject.com/helloworld.txt on null 
User-Agent: OkHttp Example 
 
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms 
Server: nginx/1.4.6 (Ubuntu) 
Content-Type: text/plain 
Content-Length: 1759 
Connection: keep-alive 

通过日志,我们可以看到OkHttp已经重定向了,可以通过引文reponse.request().url()与request.url()不同来区分。我们发现,应用拦截器只会被调用一次,并且从chain.proceed()返回的响应是重定向后的响应。

网络拦截器

注册一个网络拦截器很相似,调用addNetworkInterceptor()替代addInterceptor()。同样是上面的实例:

OkHttpClient client = new OkHttpClient.Builder() 
    .addNetworkInterceptor(new LoggingInterceptor()) 
    .build(); 
 
Request request = new Request.Builder() 
    .url("http://www.publicobject.com/helloworld.txt") 
    .header("User-Agent", "OkHttp Example") 
    .build(); 
 
Response response = client.newCall(request).execute(); 
response.body().close(); 

当我们运行这个代码,拦截器会执行两次:一次是访问http://www.publicobject.com/helloworld.txt的初始请求,另外一个是重定向到https://publicobject.com/helloworld.txt

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1} 
User-Agent: OkHttp Example 
Host: www.publicobject.com 
Connection: Keep-Alive 
Accept-Encoding: gzip 
 
INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms 
Server: nginx/1.4.6 (Ubuntu) 
Content-Type: text/html 
Content-Length: 193 
Connection: keep-alive 
Location: https://publicobject.com/helloworld.txt 
 
INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1} 
User-Agent: OkHttp Example 
Host: publicobject.com 
Connection: Keep-Alive 
Accept-Encoding: gzip 
 
INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms 
Server: nginx/1.4.6 (Ubuntu) 
Content-Type: text/plain 
Content-Length: 1759 
Connection: keep-alive 

网络请求也包含更多数据,例如通过OkHttp添加的Accept-Encoding:gzip头来通知支持响应压缩。网络拦截器的Chain有一个非空Connection,可以用来访问IP地址和用来连接网络服务器的TLS配置。

应用拦截器VS网络拦截器

选择哪种拦截器需要根据实际情况,每种拦截器chain都有自己相对的优势。

应用拦截器

  • 不需要关心像重定向和重试这样的中间响应;
  • 总是调用一次,即使HTTP响应从缓存中获取服务;
  • 监视应用原始意图。不关心OkHttp注入的像If-None-Match头;
  • 允许短路并不调用Chain.proceed();
  • 允许重试并执行多个Chain.proceed()调用。

网络拦截器

  • 可以操作像重定向和重试这样的中间响应;
  • 对于短路网络的缓存响应不会调用;
  • 监视即将要通过网络传输的数据;
  • 访问运输请求的Connection。

重写请求

拦截器支持添加,移除或替换请求头,如果有请求主体,它们也可以改变。例如,如果你连接一个已知支持请求主体压缩的网络服务器,你还可以使用一个应用拦截器来添加请求主体压缩。

final class GzipRequestInterceptor implements Interceptor { 
  @Override public Response intercept(Interceptor.Chain chain) throws IOException { 
    Request originalRequest = chain.request(); 
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) { 
      return chain.proceed(originalRequest); 
    } 
 
    Request compressedRequest = originalRequest.newBuilder() 
        .header("Content-Encoding", "gzip") 
        .method(originalRequest.method(), gzip(originalRequest.body())) 
        .build(); 
    return chain.proceed(compressedRequest); 
  } 
 
  private RequestBody gzip(final RequestBody body) { 
    return new RequestBody() { 
      @Override public MediaType contentType() { 
        return body.contentType(); 
      } 
 
      @Override public long contentLength() { 
        return -1; // We don't know the compressed length in advance! 
      } 
 
      @Override public void writeTo(BufferedSink sink) throws IOException { 
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink)); 
        body.writeTo(gzipSink); 
        gzipSink.close(); 
      } 
    }; 
  } 
} 

重写响应

当然,拦截器也可以重写响应头并且改变响应主体。如果你在一个棘手的环境下并准备处理结果,重写响应头是一个解决问题强大的方式。

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() { 
  @Override public Response intercept(Interceptor.Chain chain) throws IOException { 
    Response originalResponse = chain.proceed(chain.request()); 
    return originalResponse.newBuilder() 
        .header("Cache-Control", "max-age=60") 
        .build(); 
  } 
}; 

OkHttp使用Https

关于Https及其工作的流程本文不做任何的介绍,本文主要介绍在OkHttp中如何使用Https进行网络校验即请求。在使用OkHttpClient初始化OkHttpClient对象时,有两个关键的地方需要注意:hostnameVerifier和sslSocketFactory。

OkHttpClient okHttpClient = new OkHttpClient.Builder() 
                .connectTimeout(20000L, TimeUnit.MILLISECONDS) 
                .readTimeout(20000L, TimeUnit.MILLISECONDS) 
                .addInterceptor(new LoggerInterceptor("TAG")) 
                .hostnameVerifier(new HostnameVerifier() { 
                    @Override 
                    public boolean verify(String hostname, SSLSession session) { 
                        return true; 
                    } 
                }) 
                .sslSocketFactory(sslParams.sSLSocketFactory,sslParams.trustManager) 
                .build(); 

其中sslSocketFactory传入两个参数,一个是SSLSocketFactory,另一个是TrustManager,通常都是写一个HttpsUtils,里面持有这两个对象,读取本地的一个证书,进行相关初始化赋值动作。 hostnameVerifier则是对服务端返回的一些信息进行相关校验的地方, 用于客户端判断所连接的服务端是否可信,通常默认return true。

public boolean verify(String host, X509Certificate certificate) { 
    return verifyAsIpAddress(host) 
        ? verifyIpAddress(host, certificate) 
        : verifyHostname(host, certificate); 
  } 

OkHttp的验证逻辑

对于一个android开发者来说,目前的网络请求框架大部分都是使用okhttp进行网络请求的,所以了解okhttp是如何具体工作的对于我们平时开发有很大的帮助的。当我们使用https进行网络请求的时候最终进行连接的类是RealConnection,该类的关键代码如下:

private void connectTls(int readTimeout, int writeTimeout, 
      ConnectionSpecSelector connectionSpecSelector) throws IOException { 
    Address address = route.address(); 
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory(); 
    boolean success = false; 
    SSLSocket sslSocket = null; 
    try { 
      // Create the wrapper over the connected socket. 
    //创建Socket 
      sslSocket = (SSLSocket) sslSocketFactory.createSocket( 
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */); 
      // Configure the socket's ciphers, TLS versions, and extensions. 
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket); 
      if (connectionSpec.supportsTlsExtensions()) { 
        Platform.get().configureTlsExtensions( 
            sslSocket, address.url().host(), address.protocols()); 
      } 
      // Force handshake. This can throw! 
        //初次握手 
      sslSocket.startHandshake(); 
      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession()); 
      // Verify that the socket's certificates are acceptable for the target host. 
    //校验,回调hostnameVerifier.verify方法 
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) { 
        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0); 
        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:" 
            + "/n    certificate: " + CertificatePinner.pin(cert) 
            + "/n    DN: " + cert.getSubjectDN().getName() 
            + "/n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert)); 
      } 
      // Check that the certificate pinner is satisfied by the certificates presented. 
      address.certificatePinner().check(address.url().host(), 
          unverifiedHandshake.peerCertificates()); 

在该类中,我们主要关心的地方也是在初次握手建立连接和本地校验的那,正常情况下,我们在调用https地址的时候会先连接,就是调到上面代码的位置,之后执行初次握手,回调验证服务端是否可信,然后在进行正常的网络请求。如果在这个过程中出现异常,就会报一个证书信任的问题,出现这种情况有两方面,一是客户端验证服务端,二是服务端验证客户端。

证书获取

下面介绍下证书获取的相关内容,证书校验主要用到了hostnameVerifier.verify(),该方法的源码如下:

@Override 
 public boolean verify(String hostname, SSLSession session) { 
       Certificate[] localCertificates = new Certificate[0]; 
       try { 
     //获取证书链中的所有证书 
           localCertificates = session.getPeerCertificates(); 
         } catch (SSLPeerUnverifiedException e) { 
                e.printStackTrace(); 
           } 
    //打印所有证书内容 
        for (Certificate c : localCertificates) { 
          Log.d(TAG, "verify: "+c.toString()); 
        } 
     try { 
    //将证书链中的第一个写到文件 
           createFileWithByte(localCertificates[0].getEncoded()); 
            } catch (CertificateEncodingException e) { 
              e.printStackTrace(); 
            } 
       return true; 
       } 
    //写到文件 
    private void createFileWithByte(byte[] bytes) { 
        // TODO Auto-generated method stub 
        /** 
         * 创建File对象,其中包含文件所在的目录以及文件的命名 
         */ 
        File file = new File(Environment.getExternalStorageDirectory(), 
                "ca.cer"); 
        // 创建FileOutputStream对象 
        FileOutputStream outputStream = null; 
        // 创建BufferedOutputStream对象 
        BufferedOutputStream bufferedOutputStream = null; 
        try { 
            // 如果文件存在则删除 
            if (file.exists()) { 
                file.delete(); 
            } 
            // 在文件系统中根据路径创建一个新的空文件 
            file.createNewFile(); 
            // 获取FileOutputStream对象 
            outputStream = new FileOutputStream(file); 
            // 获取BufferedOutputStream对象 
            bufferedOutputStream = new BufferedOutputStream(outputStream); 
            // 往文件所在的缓冲输出流中写byte数据 
            bufferedOutputStream.write(bytes); 
            // 刷出缓冲输出流,该步很关键,要是不执行flush()方法,那么文件的内容是空的。 
            bufferedOutputStream.flush(); 
        } catch (Exception e) { 
            // 打印异常信息 
            e.printStackTrace(); 
        } finally { 
            // 关闭创建的流对象 
            if (outputStream != null) { 
                try { 
                    outputStream.close(); 
                } catch (IOException e) { 
                    e.printStackTrace(); 
                } 
            } 
            if (bufferedOutputStream != null) { 
                try { 
                    bufferedOutputStream.close(); 
                } catch (Exception e2) { 
                    e2.printStackTrace(); 
                } 
            } 
        } 
    }

hostnameVerifier主要有两个参数,一个是hostname就是你请求地址的host,session则包括了从服务端返回的证书链。
证书链通常有三个,第一个是我们自己的,然后也能在本地看到证书文件。包含一些相关信息,包括公钥,颁发机构等,最为严苛的方式就是可以从本地读取一个证书,取公钥与服务器返回的证书公钥进行对比。

但是证书也不是完全安全的,CertificatePinner就是一个用来限制哪些证书和证书颁发机构可以被信任。证书锁定提升安全性,但是限制你的服务器团队更新他们的TLS证书的能力。例如:

public CertificatePinning() { 
    client = new OkHttpClient.Builder() 
        .certificatePinner(new CertificatePinner.Builder() 
            .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") 
            .build()) 
        .build(); 
  } 
 
  public void run() throws Exception { 
    Request request = new Request.Builder() 
        .url("https://publicobject.com/robots.txt") 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); 
 
    for (Certificate certificate : response.handshake().peerCertificates()) { 
      System.out.println(CertificatePinner.pin(certificate)); 
    } 
  } 

自定义可信任的证书

当然,也可以使用自定义的证书来替换主机的证书,然后使用sslSocketFactory函数进行设置。

private final OkHttpClient client; 
 
  public CustomTrust() { 
    SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream()); 
    client = new OkHttpClient.Builder() 
        .sslSocketFactory(sslContext.getSocketFactory()) 
        .build(); 
  } 
 
  public void run() throws Exception { 
    Request request = new Request.Builder() 
        .url("https://publicobject.com/helloworld.txt") 
        .build(); 
 
    Response response = client.newCall(request).execute(); 
    System.out.println(response.body().string()); 
  } 
 
  private InputStream trustedCertificatesInputStream() { 
    ... // Full source omitted. See sample. 
  } 
 
  public SSLContext sslContextForTrustedCertificates(InputStream in) { 
    ... // Full source omitted. See sample. 
  } 

SSLSocketFactory

安全套接层工厂,用于创建SSLSocket,默认的SSLSocket是信任手机内置信任的证书列表,我们可以通过OKHttpClient.Builder的sslSocketFactory方法定义自己的信任策略。下面是加载SSLSocketFactory的相关代码:

public static SSLSocketFactory getSSLSocketFactory(InputStream... certificates) { 
        try { 
//用我们的证书创建一个keystore 
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 
            keyStore.load(null); 
            int index = 0; 
            for (InputStream certificate : certificates) { 
                String certificateAlias = "server"+Integer.toString(index++); 
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); 
                try { 
                    if (certificate != null) { 
                        certificate.close(); 
                    } 
                } catch (IOException e) { 
                    e.printStackTrace(); 
                } 
            } 
//创建一个trustmanager,只信任我们创建的keystore 
            SSLContext sslContext = SSLContext.getInstance("TLS"); 
            TrustManagerFactory trustManagerFactory = 
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 
            trustManagerFactory.init(keyStore); 
            sslContext.init( 
                    null, 
                    trustManagerFactory.getTrustManagers(), 
                    new SecureRandom() 
            ); 
            return sslContext.getSocketFactory(); 
        } catch (Exception e) { 
            e.printStackTrace(); 
            return null; 
        } 
    } 

X509TrustManager

public interface X509TrustManager extends TrustManager { 
    void checkClientTrusted(X509Certificate[] var1, String var2) throws CertificateException; 
 
    void checkServerTrusted(X509Certificate[] var1, String var2) throws CertificateException; 
 
    X509Certificate[] getAcceptedIssuers(); 
} 

HostnameVerifier

HostnameVerifier的接口定义如下:

public interface HostnameVerifier { 
    boolean verify(String var1, SSLSession var2); 
}

这个接口主要实现对于域名的校验,OKHTTP实现了一个OkHostnameVerifier,对于证书中的IP及Host做了各种正则匹配,默认情况下使用的是这个策略。相关代码如下:

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

在实际使用中可以将上面的东西封装起来,例如:

   public class SSLSocketClient{ 
     //获取这个SSLSocketFactory   
    public static SSLSocketFactory getSSLSocketFactory(){ 
         try{ 
             SSLContext sslContext = SSLContext.getInstance("SSL"); 
            sslContext.init(null, getTrustManager(), new SecureRandom()); 
             return sslContext.getSocketFactory(); 
        } 
         catch (Exception e){ 
             throw new RuntimeException(e); 
        } 
     } 
 
   //获取TrustManager   
     private static TrustManager[] getTrustManager(){ 
         TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager(){ 
            @Override 
            public void checkClientTrusted(X509Certificate[] chain, String authType){ 
            } 
 
            @Override 
            public void checkServerTrusted(X509Certificate[] chain, String authType){ 
            } 
            @Override 
            public X509Certificate[] getAcceptedIssuers(){ 
                 return new X509Certificate[]{}; 
             } 
        }}; 
         return trustAllCerts; 
    } 
 
    //获取HostnameVerifier   
     public static HostnameVerifier getHostnameVerifier(){ 
        HostnameVerifier hostnameVerifier = new HostnameVerifier(){ 
            @Override 
            public boolean verify(String s, SSLSession sslSession){ 
                return true; 
            } 
        }; 
         return hostnameVerifier; 
     } 
 }

然后在需要使用的使用的地方

OkHttpClient.Builder builder=new OkHttpClient.Builder(); 
... 
builder.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(); 
builder.hostnameVerifier(SSLSocketClient.getHostnameVerifier();

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/5960.html

(0)
上一篇 2021年7月17日 00:28
下一篇 2021年7月17日 00:28

相关推荐

发表回复

登录后才能评论