1. 歷史上Http請求庫優缺點
在講述OkHttp之前, 我們看下沒有OkHttp的時代, 我們是如何完成http請求的.
在沒有OkHttp的日子, 我們使用 HttpURLConnection
或者 HttpClient
. 那麼這兩者都有什麼優缺點呢? 爲什麼不在繼續使用下去呢?
HttpClient
是Apache基金會的一個開源網絡庫, 功能十分強大, API數量衆多, 但是正是由於龐大的API數量使得我們很難在不破壞兼容性的情況下對它進行升級和擴展, 所以Android團隊在提升和優化HttpClient方面的工作態度並不積極.
HttpURLConnection
是一種多用途, 輕量極的HTTP客戶端, 提供的API比較簡單, 可以容易地去使用和擴展. 不過在Android 2.2版本之前, HttpURLConnection
一直存在着一些令人厭煩的bug. 比如說對一個可讀的InputStream調用close()方法時,就有可能會導致連接池失效了。那麼我們通常的解決辦法就是直接禁用掉連接池的功能:
private void disableConnectionReuseIfNecessary() { // 這是一個2.2版本之前的bug if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) { System.setProperty("http.keepAlive", "false"); } }
因此, 一般的推薦是在2.2之前, 使用 HttpClient
, 因爲其bug較少. 在2.2之後, 推薦使用 HttpURLConnection
, 因爲API簡單, 體積小, 並且有壓縮和緩存機制, 並且Android團隊後續會繼續優化 HttpURLConnection
.
但是, 上面兩個類庫和 OkHttp
比起來就弱爆了, 因爲OkHttp不僅具有高效的請求效率, 並且提供了很多開箱即用的網絡疑難雜症解決方案.
- 支持HTTP/2, HTTP/2通過使用多路複用技術在一個單獨的TCP連接上支持併發, 通過在一個連接上一次性發送多個請求來發送或接收數據
- 如果HTTP/2不可用, 連接池複用技術也可以極大減少延時
- 支持GZIP, 可以壓縮下載體積
- 響應緩存可以直接避免重複請求
- 會從很多常用的連接問題中自動恢復
- 如果您的服務器配置了多個IP地址, 當第一個IP連接失敗的時候, OkHttp會自動嘗試下一個IP
- OkHttp還處理了代理服務器問題和SSL握手失敗問題
使用 OkHttp 無需重寫您程序中的網絡代碼。OkHttp實現了幾乎和java.net.HttpURLConnection一樣的API。如果你用了 Apache HttpClient,則OkHttp也提供了一個對應的okhttp-apache 模塊。
還有一個好消息, 從Android 4.4起, 其 HttpURLConnection
的內部實現已經變爲 OkHttp
, 您可以參考這兩個網頁: 爆棧網 和 Twitter .
2. OkHttp類與http請求響應的映射
在講解OkHttp使用之前, 再看下我們Http請求和響應都有哪些部分組成.
2.1 http請求
所以一個類庫要完成一個http請求, 需要包含 請求方法
, 請求地址
, 請求協議
, 請求頭
, 請求體
這五部分. 這些都在 okhttp3.Request
的類中有體現, 這個類正是代表http請求的類. 看下圖:
其中 HttpUrl
類代表 請求地址
, String method
代表 請求方法
, Headers
代表請求頭, RequestBody
代表請求體. Object tag
這個是用來取消http請求的標誌, 這個我們先不管. 這裏也許你在疑惑, 請求協議
呢? 爲什麼沒有請求協議對應的類. 且聽我慢慢道來, 下面就會講到這個問題.
2.1.1 請求協議的協商升級
目前, Http/1.1在全世界大範圍的使用中, 直接廢棄跳到http/2肯定不現實. 不是每個用戶的瀏覽器都支持http/2的, 也不是每個服務器都打算支持http/2的, 如果我們直接發送http/2格式的協議, 服務器又不支持, 那不是掛掉了! 總不能維護一個全世界的網站列表, 表示哪些支持http/2, 哪些不支持?
爲了解決這個問題, 從稍高層次上來說, 就是爲了更方便地部署新協議, HTTP/1.1 引入了 Upgrade 機制. 這個機制在 RFC7230 的「 6.7 Upgrade 」這一節中有詳細描述.
簡單說來, 就是先問下你支持http/2麼? 如果你支持, 那麼接下來我就用http/2和你聊天. 如果你不支持, 那麼我還是用原來的http/1.1和你聊天.
1.客戶端在請求頭部中指定 Connection
和 Upgrade
兩個字段發起 HTTP/1.1 協議升級. HTTP/2 的協議名稱是 h2c, 代表 HTTP/2 ClearText.
GET / HTTP/1.1Host: example.comConnection: Upgrade, HTTP2-SettingsUpgrade: h2cHTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
2.如果服務端不同意升級或者不支持 Upgrade 所列出的協議,直接忽略即可(當成 HTTP/1.1 請求,以 HTTP/1.1 響應).
HTTP/1.1 200 OKContent-Length: 243Content-Type: text/html...
如果服務端同意升級,那麼需要這樣響應:
HTTP/1.1 101 Switching ProtocolsConnection: UpgradeUpgrade: h2c[ HTTP/2 connection ... ]
HTTP Upgrade 響應的狀態碼是 101,並且響應正文可以使用新協議定義的數據格式。
這樣就可以完成從http/1.1升級到http/2了. 同樣也可以從http/1.1升級到WebSocket.
這樣, 你就瞭解了爲什麼OkHttp沒有指定具體請求協議了吧. 因爲OkHttp使用了請求協議的協商升級, 無論是1.1還是2, 都先只以1.1來發送, 並在發送的信息頭裏包含協議升級字段. 接下來就看服務器是否支持協議升級了. OkHttp使用的協議升級字段是 ALPN
, 如果有興趣, 可以更深入的查閱相關資料.
2.1.2 OkHttp請求
接下來我們構造一個http請求, 並查看請求具體內容.
final Request request = new Request.Builder().url("https://github.com/").build();
我們看下在內存中, 這個請求是什麼樣子的, 是否如我們上文所說和 請求方法
, 請求地址
, 請求頭
, 請求體
一一對應.
2.2 http響應
我們看下一個http響應由哪些部分組成, 先看下響應組成圖:
可以看到大體由 應答首行
, 應答頭
, 應答體
構成. 但是 應答首行
表達的信息過多,HTTP/1.1
表示 訪問協議
, 200
是響應碼, OK
是描述狀態的消息. 根據單一職責, 我們不應該把這麼多內容用一個 應答首行
來表示. 這樣的話, 我們的響應就應該由 訪問協議
, 響應碼
, 描述信息
, 響應頭
, 響應體
來組成.
2.2.1 OkHttp響應
我們看下OkHttp庫怎麼表示一個響應:
可以看到 Response
類裏面有 Protocol
代表 請求協議
, int code
代表 響應碼
,String message
代表 描述信息
, Headers
代表 響應頭
, ResponseBody
代表 響應體
. 當然除此之外, 還有 Request
代表持有的請求, Handshake
代表SSL/TLS握手協議驗證時的信息, 這些額外信息我們暫時不問.
有了剛纔說的OkHttp響應的類組成, 我們看下OkHttp請求後響應在內存中的內容:
final Request request = new Request.Builder().url("https://github.com/").build();Response response = client.newCall(request).execute();
可以看到和我們的分析十分一致.
講了OkHttp裏的請求類和響應類, 我們接下來就可以直接講述OkHttp的使用方法了.
3 HTTP GET
3.1 同步GET
同步GET的意思是一直等待http請求, 直到返回了響應. 在這之間會阻塞進程, 所以通過get不能在Android的主線程中執行, 否則會報錯.
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()); }
OkHttpClient實現了 Call.Factory
接口, 是Call的工廠類, Call負責發送執行請求和讀取響應.
Request代表Http請求, 通過Request.Builder輔助類來構建.
client.newCall(request)通過傳入一個http request, 返回一個Call調用. 然後執行execute()方法, 同步獲得
Response代表Http請求的響應. response.body()是ResponseBody類, 代表響應體, 可以通過responseBody.string()獲得字符串的表達形式, 或responseBody.bytes()獲得字節數組的表達形式, 這兩種形式都會把文檔加入到內存. 也可以通過responseBody.charStream()和responseBody.byteStream()返回流來處理.
上述代碼完成的功能是下載一個文件, 打印他的響應頭, 以string形式打印響應體.
響應體的string()方法對於小文檔來說十分方便高效. 但是如果響應體太大(超過1MB), 應避免使用 string()方法, 因爲它會將把整個文檔加載到內存中.
對於超過1MB的響應body, 應使用流的方式來處理響應body. 這和我們處理xml文檔的邏輯是一致的, 小文件可以載入內存樹狀解析, 大文件就必須流式解析.
3.2 異步GET
異步GET是指在另外的工作線程中執行http請求, 請求時不會阻塞當前的線程, 所以可以在Android主線程中使用.
下面是在一個工作線程中下載文件, 當響應可讀時回調Callback接口. 當響應頭準備好後, 就會調用Callback接口, 所以讀取 響應體
時可能會阻塞. OkHttp現階段不提供異步api來接收響應體。
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(Request request, Throwable throwable) { throwable.printStackTrace(); } @Override public void onResponse(Response response) throws IOException { 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()); } }); }
4 HTTP POST
4.1 Post方式提交String
下面是使用HTTP POST提交請求到服務. 這個例子提交了一個markdown文檔到web服務, 以HTML方式渲染markdown. 因爲整個請求體都在內存中, 因此避免使用此api提交大文檔(大於1MB).
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()); }
4.2 Post方式提交流
以流的方式POST提交請求體. 請求體的內容由流寫入產生. 這個例子是流直接寫入Okio的BufferedSink. 你的程序可能會使用OutputStream, 你可以使用BufferedSink.outputStream()來獲取. OkHttp的底層對流和字節的操作都是基於Okio庫, Okio庫也是Square開發的另一個IO庫, 填補I/O和NIO的空缺, 目的是提供簡單便於使用的接口來操作IO.
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()); }
4.3 Post方式提交文件
以文件作爲請求體是十分簡單的。
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()); }
4.4 Post方式提交表單
使用FormEncodingBuilder來構建和HTML <form>
標籤相同效果的請求體. 鍵值對將使用一種HTML兼容形式的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()); }
4.5 Post方式提交分塊請求
MultipartBody.Builder可以構建複雜的請求體, 與HTML文件上傳形式兼容. 多塊請求體中每塊請求都是一個請求體, 可以定義自己的請求頭. 這些請求頭可以用來描述這塊請求, 例如它的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()); }
5. 其他用法
5.1 提取響應頭
典型的HTTP頭像是一個 Map<String, String>
: 每個字段都有一個或沒有值. 但是一些頭允許多個值, 像Guava的Multimap.
例如: HTTP響應裏面提供的Vary響應頭, 就是多值的. OkHttp的api試圖讓這些情況都適用.
當寫請求頭的時候, 使用header(name, value)可以設置唯一的name、value. 如果已經有值, 舊的將被移除, 然後添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).
當讀取響應頭時, 使用header(name)返回最後出現的name、value. 通常情況這也是唯一的name、value. 如果沒有值, 那麼header(name)將返回null. 如果想讀取字段對應的所有值, 使用headers(name)會返回一個list.
爲了獲取所有的Header, Headers類支持按index訪問.
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")); }
5.2 使用Gson來解析JSON響應
Gson是一個在JSON和Java對象之間轉換非常方便的api庫. 這裏我們用Gson來解析Github API的JSON響應.
注意: ResponseBody.charStream()使用響應頭Content-Type指定的字符集來解析響應體. 默認是UTF-8.
private final OkHttpClient client = new OkHttpClient(); private final Gson gson = new Gson(); public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/gists/c2a7c39532239ff261be") .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); Gist gist = gson.fromJson(response.body().charStream(), Gist.class); for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) { System.out.println(entry.getKey()); System.out.println(entry.getValue().content); } } static class Gist { Map<String, GistFile> files; } static class GistFile { String content; }
5.3 響應緩存
爲了緩存響應, 你需要一個你可以讀寫的緩存目錄, 和緩存大小的限制. 這個緩存目錄應該是私有的, 不信任的程序應不能讀取緩存內容.
一個緩存目錄同時擁有多個緩存訪問是錯誤的. 大多數程序只需要調用一次new OkHttp(), 在第一次調用時配置好緩存, 然後其他地方只需要調用這個實例就可以了. 否則兩個緩存示例互相干擾, 破壞響應緩存, 而且有可能會導致程序崩潰.
響應緩存使用HTTP頭作爲配置. 你可以在請求頭中添加Cache-Control: max-stale=3600 , OkHttp緩存會支持. 你的服務通過響應頭確定響應緩存多長時間, 例如使用Cache-Control: max-age=9600.
private final OkHttpClient client;public CacheResponse(File cacheDirectory) throws Exception { int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(cacheDirectory, cacheSize); client = new OkHttpClient(); client.setCache(cache); }public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); Response response1 = client.newCall(request).execute(); if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1); String response1Body = response1.body().string(); System.out.println("Response 1 response: " + response1); System.out.println("Response 1 cache response: " + response1.cacheResponse()); System.out.println("Response 1 network response: " + response1.networkResponse()); Response response2 = client.newCall(request).execute(); if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2); String response2Body = response2.body().string(); System.out.println("Response 2 response: " + response2); System.out.println("Response 2 cache response: " + response2.cacheResponse()); System.out.println("Response 2 network response: " + response2.networkResponse()); System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body)); }
如果需要阻值response使用緩存, 使用 CacheControl.FORCE_NETWORK
. 如果需要阻值response使用網絡, 使用 CacheControl.FORCE_CACHE
.
警告: 如果你使用 FORCE_CACHE
, 但是response要求使用網絡, OkHttp將會返回一個504 Unsatisfiable Request
響應.
5.3.1 Force a Network Response
有些時候, 比如用戶剛剛點擊 刷新
按鈕, 這時必須跳過緩存, 直接從服務器抓取數據. 爲了強制全面刷新, 我們需要添加 no-cache
指令:
connection.addRequestProperty("Cache-Control", "no-cache");
這樣就可以強制每次請求直接發送給源服務器, 而不經過本地緩存版本的校驗, 常用於需要確認認證的應用和嚴格要求使用最新數據的應用.
5.3.2 Force a Cache Response
有時你會想立即顯示資源. 這樣即使在後臺正下載着最新資源, 你的客戶端仍然可以先顯示原有資源, 畢竟有個東西顯示比沒有東西顯示要好.
如果需要限制讓請求優先使用本地緩存資源, 需要增加 only-if-cached
指令:
try { connection.addRequestProperty("Cache-Control", "only-if-cached"); InputStream cached = connection.getInputStream(); // the resource was cached! show it catch (FileNotFoundException e) { // the resource was not cached } }
5.4 取消一個Call
使用Call.cancel()可以立即停止掉一個正在執行的call. 如果一個線程正在寫請求或者讀響應, 將會引發IOException. 當call沒有必要的時候, 使用這個api可以節約網絡資源. 例如當用戶離開一個應用時, 不管同步還是異步的call都可以取消.
你可以通過tags來同時取消多個請求. 當你構建一請求時, 使用RequestBuilder.tag(tag)來分配一個標籤, 之後你就可以用OkHttpClient.cancel(tag)來取消所有帶有這個tag的call.
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); } }
5.5 超時
沒有響應時使用超時結束call. 沒有響應的原因可能是客戶點鏈接問題、服務器可用性問題或者這之間的其他東西. OkHttp支持連接超時, 讀取超時和寫入超時.
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); }
5.6 每個call的配置
使用OkHttpClient, 所有的HTTP Client配置包括代理設置、超時設置、緩存設置. 當你需要爲單個call改變配置的時候, 調用 OkHttpClient.newBuilder()
. 這個api將會返回一個builder, 這個builder和原始的client共享相同的連接池, 分發器和配置.
下面的例子中,我們讓一個請求是500ms的超時、另一個是3000ms的超時。
private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay. .build(); try { // Copy to customize OkHttp for this request. OkHttpClient copy = client.newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build(); Response response = copy.newCall(request).execute(); System.out.println("Response 1 succeeded: " + response); } catch (IOException e) { System.out.println("Response 1 failed: " + e); } try { // Copy to customize OkHttp for this request. OkHttpClient copy = client.newBuilder() .readTimeout(3000, TimeUnit.MILLISECONDS) .build(); Response response = copy.newCall(request).execute(); System.out.println("Response 2 succeeded: " + response); } catch (IOException e) { System.out.println("Response 2 failed: " + e); } }
5.7 處理驗證
這部分和HTTP AUTH有關.
5.7.1 HTTP AUTH
使用HTTP AUTH需要在server端配置http auth信息, 其過程如下:
- 客戶端發送http請求
- 服務器發現配置了http auth, 於是檢查request裏面有沒有”Authorization”的http header
- 如果有, 則判斷Authorization裏面的內容是否在用戶列表裏面, Authorization header的典型數據爲”Authorization: Basic jdhaHY0=”, 其中Basic表示基礎認證, jdhaHY0=是base64編碼的”user:passwd”字符串. 如果沒有,或者用戶密碼不對,則返回http code 401頁面給客戶端.
- 標準的http瀏覽器在收到401頁面之後, 應該彈出一個對話框讓用戶輸入帳號密碼; 並在用戶點確認的時候再次發出請求, 這次請求裏面將帶上Authorization header.
一次典型的訪問場景是:
瀏覽器發送http請求(沒有Authorization header)
服務器端返回401頁面
瀏覽器彈出認證對話框
用戶輸入帳號密碼,並點確認
瀏覽器再次發出http請求(帶着Authorization header)
服務器端認證通過,並返回頁面
瀏覽器顯示頁面
5.7.2 OkHttp認證
OkHttp會自動重試未驗證的請求. 當響應是 401 Not Authorized
時, Authenticator
會被要求提供證書. Authenticator的實現中需要建立一個新的包含證書的請求. 如果沒有證書可用, 返回null來跳過嘗試.
使用 Response.challenges()
來獲得任何 authentication challenges
的 schemes 和 realms. 當完成一個 Basic challenge
, 使用 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()); }
當認證無法工作時, 爲了避免多次重試, 你可以返回空來放棄認證. 例如, 當 exact credentials
已經嘗試過, 你可能會直接想跳過認證, 可以這樣做:
if (credential.equals(response.request().header("Authorization"))) { return null; // If we already failed with these credentials, don't retry. }
當重試次數超過定義的次數, 你若想跳過認證, 可以這樣做:
if (responseCount(response) >= 3) { return null; // If we've failed 3 times, give up. } private int responseCount(Response response) { int result = 1; while ((response = response.priorResponse()) != null) { result++; } return result; }