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

2021-03-09 10:11 更新

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

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

  1. HttpClient httpClient = new HttpClient();

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

  1. HttpClientRequest request = await httpClient.getUrl(uri);

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

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

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

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

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

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

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

  1. HttpClientResponse response = await request.close();

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

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

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

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

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

  1. httpClient.close();

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

#示例

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

圖11-1

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

  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'package:flutter/material.dart';
  4. class HttpTestRoute extends StatefulWidget {
  5. @override
  6. _HttpTestRouteState createState() => new _HttpTestRouteState();
  7. }
  8. class _HttpTestRouteState extends State<HttpTestRoute> {
  9. bool _loading = false;
  10. String _text = "";
  11. @override
  12. Widget build(BuildContext context) {
  13. return ConstrainedBox(
  14. constraints: BoxConstraints.expand(),
  15. child: SingleChildScrollView(
  16. child: Column(
  17. children: <Widget>[
  18. RaisedButton(
  19. child: Text("獲取百度首頁"),
  20. onPressed: _loading ? null : () async {
  21. setState(() {
  22. _loading = true;
  23. _text = "正在請(qǐng)求...";
  24. });
  25. try {
  26. //創(chuàng)建一個(gè)HttpClient
  27. HttpClient httpClient = new HttpClient();
  28. //打開Http連接
  29. HttpClientRequest request = await httpClient.getUrl(
  30. Uri.parse("https://www.baidu.com"));
  31. //使用iPhone的UA
  32. 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");
  33. //等待連接服務(wù)器(會(huì)將請(qǐng)求信息發(fā)送給服務(wù)器)
  34. HttpClientResponse response = await request.close();
  35. //讀取響應(yīng)內(nèi)容
  36. _text = await response.transform(utf8.decoder).join();
  37. //輸出響應(yīng)頭
  38. print(response.headers);
  39. //關(guān)閉client后,通過該client發(fā)起的所有請(qǐng)求都會(huì)中止。
  40. httpClient.close();
  41. } catch (e) {
  42. _text = "請(qǐng)求失?。?e";
  43. } finally {
  44. setState(() {
  45. _loading = false;
  46. });
  47. }
  48. }
  49. ),
  50. Container(
  51. width: MediaQuery.of(context).size.width-50.0,
  52. child: Text(_text.replaceAll(new RegExp(r"\s"), ""))
  53. )
  54. ],
  55. ),
  56. ),
  57. );
  58. }
  59. }

控制臺(tái)輸出:

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

#HttpClient配置

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

屬性 含義
idleTimeout 對(duì)應(yīng)請(qǐng)求頭中的 keep-alive 字段值,為了避免頻繁建立連接,httpClient 在請(qǐng)求結(jié)束后會(huì)保持連接一段時(shí)間,超過這個(gè)閾值后才會(huì)關(guān)閉連接。
connectionTimeout 和服務(wù)器建立連接的超時(shí),如果超過這個(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ì)于這些屬性,你完全可以通過HttpClientRequest直接設(shè)置 header,不同的是通過HttpClient設(shè)置的對(duì)整個(gè)httpClient都生效,而通過HttpClientRequest設(shè)置的只對(duì)當(dāng)前請(qǐng)求生效。

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

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

image-20181031114207514

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

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

  1. WWW-Authenticate: Basic realm="admin"

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

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

  1. Authorization: Basic YXXFISDJFISJFGIJIJG

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

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

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

  1. 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()來動(dòng)態(tài)添加用戶憑證,例如:

  1. httpClient.authenticate=(Uri url, String scheme, String realm) async{
  2. if(url.host=="xx.com" && realm=="admin"){
  3. httpClient.addCredentials(url,
  4. "admin",
  5. new HttpClientBasicCredentials("username","pwd"),
  6. );
  7. return true;
  8. }
  9. return false;
  10. };

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

#代理

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

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

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

在 APP 開發(fā)中,很多時(shí)候我們需要抓包來調(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)證方法和屬性:

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

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

#證書校驗(yàn)

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

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

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

#示例

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

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

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

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

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

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

#總結(jié)

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

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)