Dart IO 庫中提供了用于發(fā)起 Http 請求的一些類,我們可以直接使用HttpClient
來發(fā)起請求。使用HttpClient
發(fā)起請求分為五步:
HttpClient
: HttpClient httpClient = new HttpClient();
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); //可以直接添加輸入流
HttpClientResponse response = await request.close();
這一步完成后,請求信息就已經(jīng)發(fā)送給服務(wù)器了,返回一個HttpClientResponse
對象,它包含響應(yīng)頭(header)和響應(yīng)流(響應(yīng)體的 Stream),接下來就可以通過讀取響應(yīng)流來獲取響應(yīng)內(nèi)容。
String responseBody = await response.transform(utf8.decoder).join();
我們通過讀取響應(yīng)流來獲取服務(wù)器返回的數(shù)據(jù),在讀取時我們可以設(shè)置編碼格式,這里是 utf8。
HttpClient
: httpClient.close();
關(guān)閉 client 后,通過該 client 發(fā)起的所有請求都會中止。
我們實現(xiàn)一個獲取百度首頁 html 的例子,示例效果如圖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
有很多屬性可以配置,常用的屬性列表如下:
屬性 | 含義 |
---|---|
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)前請求生效。
Htt 協(xié)議的認(rèn)證(Authentication)機制可以用于保護非公開資源。如果 Http 服務(wù)器開啟了認(rèn)證,那么用戶在發(fā)起請求時就需要攜帶用戶憑據(jù),如果你在瀏覽器中訪問了啟用 Basic 認(rèn)證的資源時,瀏覽就會彈出一個登錄框,如:
我們先看看 Basic 認(rèn)證的基本過程:
WWW-Authenticate: Basic realm="admin"
其中"Basic"為認(rèn)證方式,realm 為用戶角色的分組,可以在后臺添加分組。
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)證的方法和屬性:
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")
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é)中介紹的addCredentials
和authenticate
相同,故不再贅述。
Https 中為了防止通過偽造證書而發(fā)起的中間人攻擊,客戶端應(yīng)該對自簽名或非 CA 頒發(fā)的證書進行校驗。HttpClient
對證書校驗的邏輯如下:
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 格式的證書。
值得注意的是,HttpClient
提供的這些屬性和方法最終都會作用在請求 header 里,我們完全可以通過手動去設(shè)置 header 來實現(xiàn),之所以提供這些方法,只是為了方便開發(fā)者而已。另外,Http 協(xié)議是一個非常重要的、使用最多的網(wǎng)絡(luò)協(xié)議,每一個開發(fā)者都應(yīng)該對 http 協(xié)議非常熟悉。
更多建議: