Dart IO 庫(kù)中提供了用于發(fā)起 Http 請(qǐng)求的一些類,我們可以直接使用HttpClient
來(lái)發(fā)起請(qǐng)求。使用HttpClient
發(fā)起請(qǐng)求分為五步:
HttpClient
: HttpClient httpClient = new HttpClient();
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); //可以直接添加輸入流
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)容。
String responseBody = await response.transform(utf8.decoder).join();
我們通過(guò)讀取響應(yīng)流來(lái)獲取服務(wù)器返回的數(shù)據(jù),在讀取時(shí)我們可以設(shè)置編碼格式,這里是 utf8。
HttpClient
: httpClient.close();
關(guān)閉 client 后,通過(guò)該 client 發(fā)起的所有請(qǐng)求都會(huì)中止。
我們實(shí)現(xiàn)一個(gè)獲取百度首頁(yè) html 的例子,示例效果如圖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
有很多屬性可以配置,常用的屬性列表如下:
屬性 | 含義 |
---|---|
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)求生效。
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è)登錄框,如:
我們先看看 Basic 認(rèn)證的基本過(guò)程:
WWW-Authenticate: Basic realm="admin"
其中"Basic"為認(rèn)證方式,realm 為用戶角色的分組,可以在后臺(tái)添加分組。
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)證的方法和屬性:
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))
這是一個(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é)中介紹的addCredentials
和authenticate
相同,故不再贅述。
Https 中為了防止通過(guò)偽造證書(shū)而發(fā)起的中間人攻擊,客戶端應(yīng)該對(duì)自簽名或非 CA 頒發(fā)的證書(shū)進(jìn)行校驗(yàn)。HttpClient
對(duì)證書(shū)校驗(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ū)。
值得注意的是,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é)議非常熟悉。
更多建議: