Flutter實(shí)戰(zhàn) 通過(guò)HttpClient發(fā)起HTTP請(qǐng)求

2021-03-09 10:11 更新

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

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

    HttpClient httpClient = new HttpClient();

  1. 打開(kāi) Http 連接,設(shè)置請(qǐng)求頭:

   HttpClientRequest request = await httpClient.getUrl(uri);

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

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

通過(guò)HttpClientRequest可以設(shè)置請(qǐng)求 header,如:

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

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

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

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

   HttpClientResponse response = await request.close();

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

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

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

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

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

   httpClient.close();

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

#示例

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

圖11-1

點(diǎn)擊“獲取百度首頁(yè)”按鈕后,會(huì)請(qǐng)求百度首頁(yè),請(qǐng)求成功后,我們將返回內(nèi)容顯示出來(lái)并在控制臺(tá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("獲取百度首頁(yè)"),
                onPressed: _loading ? null : () async {
                  setState(() {
                    _loading = true;
                    _text = "正在請(qǐng)求...";
                  });
                  try {
                    //創(chuàng)建一個(gè)HttpClient
                    HttpClient httpClient = new HttpClient();
                    //打開(kāi)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ù)器(會(huì)將請(qǐng)求信息發(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后,通過(guò)該client發(fā)起的所有請(qǐng)求都會(huì)中止。
                    httpClient.close();


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

控制臺(tái)輸出:

I/flutter (18545): connection: Keep-Alive
I/flutter (18545): cache-control: no-cache
I/flutter (18545): set-cookie: ....  //有多個(gè),省略...
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 對(duì)應(yīng)請(qǐng)求頭中的 keep-alive 字段值,為了避免頻繁建立連接,httpClient 在請(qǐng)求結(jié)束后會(huì)保持連接一段時(shí)間,超過(guò)這個(gè)閾值后才會(huì)關(guān)閉連接。
connectionTimeout 和服務(wù)器建立連接的超時(shí),如果超過(guò)這個(gè)值則會(huì)拋出 SocketException 異常。
maxConnectionsPerHost 同一個(gè) host,同時(shí)允許建立連接的最大數(shù)量。
autoUncompress 對(duì)應(yīng)請(qǐng)求頭中的 Content-Encoding,如果設(shè)置為 true,則請(qǐng)求頭中Content-Encoding 的值為當(dāng)前 HttpClient 支持的壓縮算法列表,目前只有"gzip"
userAgent 對(duì)應(yīng)請(qǐng)求頭中的 User-Agent 字段。

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

#HTTP請(qǐng)求認(rèn)證

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

image-20181031114207514

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

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

   WWW-Authenticate: Basic realm="admin"

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

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

   Authorization: Basic YXXFISDJFISJFGIJIJG

服務(wù)器驗(yàn)證用戶憑據(jù),如果通過(guò)就返回資源內(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ù)時(shí),對(duì)于用戶憑據(jù)的內(nèi)容,前者只是簡(jiǎn)單的通過(guò) Base64 編碼(可逆),而后者會(huì)進(jìn)行哈希運(yùn)算,相對(duì)來(lái)說(shuō)安全一點(diǎn)點(diǎn),但是為了安全起見(jiàn),無(wú)論是采用 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))

這是一個(gè) setter,類型是一個(gè)回調(diào),當(dāng)服務(wù)器需要用戶憑據(jù)且該用戶憑據(jù)未被添加時(shí),httpClient會(huì)調(diào)用此回調(diào),在這個(gè)回調(diào)當(dāng)中,一般會(huì)調(diào)用addCredential()來(lái)動(dòng)態(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;
   };

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

#代理

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

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

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

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

有時(shí)代理服務(wù)器也啟用了身份驗(yàn)證,這和 http 協(xié)議的認(rèn)證是相似的,HttpClient 提供了對(duì)應(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請(qǐng)求認(rèn)證”一節(jié)中介紹的addCredentialsauthenticate 相同,故不再贅述。

#證書(shū)校驗(yàn)

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

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

綜上所述,我們的證書(shū)校驗(yàn)其實(shí)就是提供一個(gè)badCertificateCallback回調(diào),下面通過(guò)一個(gè)示例來(lái)說(shuō)明。

#示例

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

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

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

對(duì)于自簽名的證書(shū),我們也可以將其添加到本地證書(shū)信任鏈中,這樣證書(shū)驗(yàn)證時(shí)就會(huì)自動(dòng)通過(guò),而不會(huì)再走到badCertificateCallback回調(diào)中:

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

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

#總結(jié)

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)