Flutter實戰(zhàn) 通過HttpClient發(fā)起HTTP請求

2021-03-09 10:11 更新

Dart IO 庫中提供了用于發(fā)起 Http 請求的一些類,我們可以直接使用HttpClient來發(fā)起請求。使用HttpClient發(fā)起請求分為五步:

  1. 創(chuàng)建一個HttpClient

    HttpClient httpClient = new HttpClient();

  1. 打開 Http 連接,設(shè)置請求頭:

   HttpClientRequest request = await httpClient.getUrl(uri);

這一步可以使用任意 Http Method,如httpClient.post(...)httpClient.delete(...)等。如果包含 Query 參數(shù),可以在構(gòu)建 uri 時添加,如:

   Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
       "xx":"xx",
       "yy":"dd"
     });

通過HttpClientRequest可以設(shè)置請求 header,如:

   request.headers.add("user-agent", "test");

如果是 post 或 put 等可以攜帶請求體方法,可以通過 HttpClientRequest 對象發(fā)送 request body,如:

   String payload="...";
   request.add(utf8.encode(payload)); 
   //request.addStream(_inputStream); //可以直接添加輸入流

  1. 等待連接服務(wù)器:

   HttpClientResponse response = await request.close();

這一步完成后,請求信息就已經(jīng)發(fā)送給服務(wù)器了,返回一個HttpClientResponse對象,它包含響應(yīng)頭(header)和響應(yīng)流(響應(yīng)體的 Stream),接下來就可以通過讀取響應(yīng)流來獲取響應(yīng)內(nèi)容。

  1. 讀取響應(yīng)內(nèi)容:

   String responseBody = await response.transform(utf8.decoder).join();

我們通過讀取響應(yīng)流來獲取服務(wù)器返回的數(shù)據(jù),在讀取時我們可以設(shè)置編碼格式,這里是 utf8。

  1. 請求結(jié)束,關(guān)閉HttpClient

   httpClient.close();

關(guān)閉 client 后,通過該 client 發(fā)起的所有請求都會中止。

#示例

我們實現(xiàn)一個獲取百度首頁 html 的例子,示例效果如圖11-1所示:

圖11-1

點擊“獲取百度首頁”按鈕后,會請求百度首頁,請求成功后,我們將返回內(nèi)容顯示出來并在控制臺打印響應(yīng) header,代碼如下:

import 'dart:convert';
import 'dart:io';


import 'package:flutter/material.dart';


class HttpTestRoute extends StatefulWidget {
  @override
  _HttpTestRouteState createState() => new _HttpTestRouteState();
}


class _HttpTestRouteState extends State<HttpTestRoute> {
  bool _loading = false;
  String _text = "";


  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints.expand(),
      child: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            RaisedButton(
                child: Text("獲取百度首頁"),
                onPressed: _loading ? null : () async {
                  setState(() {
                    _loading = true;
                    _text = "正在請求...";
                  });
                  try {
                    //創(chuàng)建一個HttpClient
                    HttpClient httpClient = new HttpClient();
                    //打開Http連接
                    HttpClientRequest request = await httpClient.getUrl(
                        Uri.parse("https://www.baidu.com"));
                    //使用iPhone的UA
                    request.headers.add("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1");
                    //等待連接服務(wù)器(會將請求信息發(fā)送給服務(wù)器)
                    HttpClientResponse response = await request.close();
                    //讀取響應(yīng)內(nèi)容
                    _text = await response.transform(utf8.decoder).join();
                    //輸出響應(yīng)頭
                    print(response.headers);


                    //關(guān)閉client后,通過該client發(fā)起的所有請求都會中止。
                    httpClient.close();


                  } catch (e) {
                    _text = "請求失?。?e";
                  } finally {
                    setState(() {
                      _loading = false;
                    });
                  }
                }
            ),
            Container(
                width: MediaQuery.of(context).size.width-50.0,
                child: Text(_text.replaceAll(new RegExp(r"\s"), ""))
            )
          ],
        ),
      ),
    );
  }
}

控制臺輸出:

I/flutter (18545): connection: Keep-Alive
I/flutter (18545): cache-control: no-cache
I/flutter (18545): set-cookie: ....  //有多個,省略...
I/flutter (18545): transfer-encoding: chunked
I/flutter (18545): date: Tue, 30 Oct 2018 10:00:52 GMT
I/flutter (18545): content-encoding: gzip
I/flutter (18545): vary: Accept-Encoding
I/flutter (18545): strict-transport-security: max-age=172800
I/flutter (18545): content-type: text/html;charset=utf-8
I/flutter (18545): tracecode: 00525262401065761290103018, 00522983

#HttpClient配置

HttpClient有很多屬性可以配置,常用的屬性列表如下:

屬性 含義
idleTimeout 對應(yīng)請求頭中的 keep-alive 字段值,為了避免頻繁建立連接,httpClient 在請求結(jié)束后會保持連接一段時間,超過這個閾值后才會關(guān)閉連接。
connectionTimeout 和服務(wù)器建立連接的超時,如果超過這個值則會拋出 SocketException 異常。
maxConnectionsPerHost 同一個 host,同時允許建立連接的最大數(shù)量。
autoUncompress 對應(yīng)請求頭中的 Content-Encoding,如果設(shè)置為 true,則請求頭中Content-Encoding 的值為當(dāng)前 HttpClient 支持的壓縮算法列表,目前只有"gzip"
userAgent 對應(yīng)請求頭中的 User-Agent 字段。

可以發(fā)現(xiàn),有些屬性只是為了更方便的設(shè)置請求頭,對于這些屬性,你完全可以通過HttpClientRequest直接設(shè)置 header,不同的是通過HttpClient設(shè)置的對整個httpClient都生效,而通過HttpClientRequest設(shè)置的只對當(dāng)前請求生效。

#HTTP請求認(rèn)證

Htt 協(xié)議的認(rèn)證(Authentication)機制可以用于保護非公開資源。如果 Http 服務(wù)器開啟了認(rèn)證,那么用戶在發(fā)起請求時就需要攜帶用戶憑據(jù),如果你在瀏覽器中訪問了啟用 Basic 認(rèn)證的資源時,瀏覽就會彈出一個登錄框,如:

image-20181031114207514

我們先看看 Basic 認(rèn)證的基本過程:

  1. 客戶端發(fā)送 http 請求給服務(wù)器,服務(wù)器驗證該用戶是否已經(jīng)登錄驗證過了,如果沒有的話, 服務(wù)器會返回一個 401 Unauthozied 給客戶端,并且在響應(yīng) header 中添加一個 “WWW-Authenticate” 字段,例如:

   WWW-Authenticate: Basic realm="admin"

其中"Basic"為認(rèn)證方式,realm 為用戶角色的分組,可以在后臺添加分組。

  1. 客戶端得到響應(yīng)碼后,將用戶名和密碼進行 base64 編碼(格式為用戶名:密碼),設(shè)置請求頭 Authorization,繼續(xù)訪問 :

   Authorization: Basic YXXFISDJFISJFGIJIJG

服務(wù)器驗證用戶憑據(jù),如果通過就返回資源內(nèi)容。

注意,Http 的方式除了 Basic 認(rèn)證之外還有:Digest 認(rèn)證、Client 認(rèn)證、Form Based 認(rèn)證等,目前 Flutter 的 HttpClient 只支持 Basic 和 Digest 兩種認(rèn)證方式,這兩種認(rèn)證方式最大的區(qū)別是發(fā)送用戶憑據(jù)時,對于用戶憑據(jù)的內(nèi)容,前者只是簡單的通過 Base64 編碼(可逆),而后者會進行哈希運算,相對來說安全一點點,但是為了安全起見,無論是采用 Basic 認(rèn)證還是 Digest 認(rèn)證,都應(yīng)該在 Https 協(xié)議下,這樣可以防止抓包和中間人攻擊。

HttpClient關(guān)于 Http 認(rèn)證的方法和屬性:

  1. addCredentials(Uri url, String realm, HttpClientCredentials credentials)

該方法用于添加用戶憑據(jù),如:

   httpClient.addCredentials(_uri,
    "admin", 
     new HttpClientBasicCredentials("username","password"), //Basic認(rèn)證憑據(jù)
   );

如果是 Digest 認(rèn)證,可以創(chuàng)建 Digest 認(rèn)證憑據(jù):

   HttpClientDigestCredentials("username","password")

  1. authenticate(Future<bool> f(Uri url, String scheme, String realm))

這是一個 setter,類型是一個回調(diào),當(dāng)服務(wù)器需要用戶憑據(jù)且該用戶憑據(jù)未被添加時,httpClient會調(diào)用此回調(diào),在這個回調(diào)當(dāng)中,一般會調(diào)用addCredential()來動態(tài)添加用戶憑證,例如:

   httpClient.authenticate=(Uri url, String scheme, String realm) async{
     if(url.host=="xx.com" && realm=="admin"){
       httpClient.addCredentials(url,
         "admin",
         new HttpClientBasicCredentials("username","pwd"), 
       );
       return true;
     }
     return false;
   };

一個建議是,如果所有請求都需要認(rèn)證,那么應(yīng)該在 HttpClient 初始化時就調(diào)用addCredentials()來添加全局憑證,而不是去動態(tài)添加。

#代理

可以通過findProxy來設(shè)置代理策略,例如,我們要將所有請求通過代理服務(wù)器(192.168.1.2:8888)發(fā)送出去:

  client.findProxy = (uri) {
    // 如果需要過濾uri,可以手動判斷
    return "PROXY 192.168.1.2:8888";
 };

findProxy 回調(diào)返回值是一個遵循瀏覽器 PAC 腳本格式的字符串,詳情可以查看 API 文檔,如果不需要代理,返回"DIRECT"即可。

在 APP 開發(fā)中,很多時候我們需要抓包來調(diào)試,而抓包軟件(如 charles)就是一個代理,這時我們就可以將請求發(fā)送到我們的抓包軟件,我們就可以在抓包軟件中看到請求的數(shù)據(jù)了。

有時代理服務(wù)器也啟用了身份驗證,這和 http 協(xié)議的認(rèn)證是相似的,HttpClient 提供了對應(yīng)的 Proxy 認(rèn)證方法和屬性:

set authenticateProxy(
    Future<bool> f(String host, int port, String scheme, String realm));
void addProxyCredentials(
    String host, int port, String realm, HttpClientCredentials credentials);

他們的使用方法和上面“HTTP請求認(rèn)證”一節(jié)中介紹的addCredentialsauthenticate 相同,故不再贅述。

#證書校驗

Https 中為了防止通過偽造證書而發(fā)起的中間人攻擊,客戶端應(yīng)該對自簽名或非 CA 頒發(fā)的證書進行校驗。HttpClient對證書校驗的邏輯如下:

  1. 如果請求的 Https 證書是可信 CA 頒發(fā)的,并且訪問 host 包含在證書的 domain 列表中(或者符合通配規(guī)則)并且證書未過期,則驗證通過。
  2. 如果第一步驗證失敗,但在創(chuàng)建 HttpClient 時,已經(jīng)通過 SecurityContext 將證書添加到證書信任鏈中,那么當(dāng)服務(wù)器返回的證書在信任鏈中的話,則驗證通過。
  3. 如果1、2驗證都失敗了,如果用戶提供了badCertificateCallback回調(diào),則會調(diào)用它,如果回調(diào)返回true,則允許繼續(xù)鏈接,如果返回false,則終止鏈接。

綜上所述,我們的證書校驗其實就是提供一個badCertificateCallback回調(diào),下面通過一個示例來說明。

#示例

假設(shè)我們的后臺服務(wù)使用的是自簽名證書,證書格式是 PEM 格式,我們將證書的內(nèi)容保存在本地字符串中,那么我們的校驗邏輯如下:

String PEM="XXXXX";//可以從文件讀取
...
httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
  if(cert.pem==PEM){
    return true; //證書一致,則允許發(fā)送數(shù)據(jù)
  }
  return false;
};

X509Certificate是證書的標(biāo)準(zhǔn)格式,包含了證書除私鑰外所有信息,讀者可以自行查閱文檔。另外,上面的示例沒有校驗 host,是因為只要服務(wù)器返回的證書內(nèi)容和本地的保存一致就已經(jīng)能證明是我們的服務(wù)器了(而不是中間人),host 驗證通常是為了防止證書和域名不匹配。

對于自簽名的證書,我們也可以將其添加到本地證書信任鏈中,這樣證書驗證時就會自動通過,而不會再走到badCertificateCallback回調(diào)中:

SecurityContext sc=new SecurityContext();
//file為證書路徑
sc.setTrustedCertificates(file);
//創(chuàng)建一個HttpClient
HttpClient httpClient = new HttpClient(context: sc);

注意,通過setTrustedCertificates()設(shè)置的證書格式必須為 PEM 或 PKCS12,如果證書格式為 PKCS12,則需將證書密碼傳入,這樣則會在代碼中暴露證書密碼,所以客戶端證書校驗不建議使用 PKCS12 格式的證書。

#總結(jié)

值得注意的是,HttpClient提供的這些屬性和方法最終都會作用在請求 header 里,我們完全可以通過手動去設(shè)置 header 來實現(xiàn),之所以提供這些方法,只是為了方便開發(fā)者而已。另外,Http 協(xié)議是一個非常重要的、使用最多的網(wǎng)絡(luò)協(xié)議,每一個開發(fā)者都應(yīng)該對 http 協(xié)議非常熟悉。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號