1. HttpClient 基本使用
HttpClient是dart自带的请求类,在io包中,实现了基本的网络请求相关的操作。
网络调用通常遵循如下步骤:
- 1.创建 client.
- 2.构造 Uri.
- 3.发起请求, 等待请求,同时您也可以配置请求headers、 body。
- 4.关闭请求, 等待响应.
- 5.解码响应的内容.
- 6.关闭 client (关闭client后,通过该client发起的所有请求都会中止)
1.1 get 请求
void getRequest() async {
// 1.创建HttpClient对象
HttpClient httpClient = HttpClient();
// 2.构建请求的uri
final url =
Uri.https("httpbin.org", "/get", {"usernaem": "mshi", "age": "20"});
// 3.构建请求
final HttpClientRequest request = await httpClient.getUrl(url);
// 4.发送请求
final response = await request.close();
if (response.statusCode == HttpStatus.ok) {
String resp = await response.transform(utf8.decoder).join();
print("get: $resp");
} else {
print(response.statusCode);
}
// 5.close client
//关闭client后,通过该client发起的所有请求都会中止。
httpClient.close();
}
1.2 post 请求
void postRequest() async {
// 1.创建HttpClient对象
HttpClient httpClient = HttpClient();
// 2.构建请求的uri
Uri url = Uri(
scheme: "https",
host: "httpbin.org",
path: "/post",
queryParameters: {"name": "mshi", "age": "20"});
// 3.构建请求
HttpClientRequest request = await httpClient.postUrl(url);
// 4.发送请求
HttpClientResponse response = await request.close();
if (response.statusCode == HttpStatus.ok) {
String resp = await response.transform(utf8.decoder).join();
print("post:$resp");
} else {
print(response.statusCode);
}
// 5.close client
//关闭client后,通过该client发起的所有请求都会中止。
httpClient.close();
}
1.3 示例
class _MSHttpClientBasicPageState extends State<MSHttpClientBasicPage> {
bool _loading = false;
String _requestText = "";
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
ElevatedButton(
onPressed: _loading ? null : requestData,
child: Text("获取百度首页")),
Container(
child: Text(_requestText.replaceAll(RegExp(r'\s'), "")),
width: MediaQuery.of(context).size.width - 50,
),
],
),
),
);
}
void requestData() async {
setState(() {
_loading = true;
});
try {
// 1.创建HttpClient对象
HttpClient httpClient = HttpClient();
// 2.构建请求的uri
Uri url = Uri.parse("https://www.baidu.com");
// 3.构建请求
HttpClientRequest request = await httpClient.getUrl(url);
// 4.添加header 非必需
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");
// 5.等待连接服务器(会将请求信息发送给服务器)
HttpClientResponse response = await request.close();
// 6.读取响应内容
_requestText = await response.transform(utf8.decoder).join();
// 7.关闭client后,通过该client发起的所有请求都会中止。
httpClient.close();
} catch (e) {
_requestText = "请求失败";
} finally {
setState(() {
_loading = false;
});
}
}
}
2. Uri
factory Uri(
{String? scheme,
String? userInfo,
String? host,
int? port,
String? path,
Iterable<String>? pathSegments,
String? query,
Map<String, dynamic /*String|Iterable<String>*/ >? queryParameters,
String? fragment}) = _Uri;
成员变量 | 说明 |
---|---|
scheme | 如网络请求时为http或https,文件请求时为file |
userInfo | 权限组件的用户信息部分,此部分默认为空 |
host | 主机部分,不区分大小写 |
port | 端口号,http请求时为80,https请求时为443 |
path | 接口请求路径(方法) |
pathSegments | 同path,只不过将path分段了 |
queryParameters | 请求参数 |
query | 同queryParameters,将queryParameters合并成一个 |
3. HttpClient配置
HttpClient有很多属性可以配置,常用的属性列表如下:
属性 | 含义 |
---|---|
idleTimeout | 对应请求头中的keep-alive字段值,为了避免频繁建立连接,httpClient在请求结束后会保持连接一段时间,超过这个阈值后才会关闭连接。 |
connectionTimeout | 和服务器建立连接的超时,如果超过这个值则会抛出SocketException异常。 |
maxConnectionsPerHost | 同一个host,同时允许建立连接的最大数量。 |
autoUncompress | 对应请求头中的Content-Encoding,如果设置为true,则请求头中Content-Encoding的值为当前HttpClient支持的压缩算法列表,目前只有"gzip" |
userAgent | 对应请求头中的User-Agent字段。 |
可以发现,有些属性只是为了更方便的设置请求头,对于这些属性,你完全可以通过HttpClientRequest
直接设置header,不同的是通过HttpClient
设置的对整个httpClient
都生效,而通过HttpClientRequest
设置的只对当前请求生效。
4. HTTP请求认证
Http协议的认证(Authentication)机制可以用于保护非公开资源。如果Http服务器开启了认证,那么用户在发起请求时就需要携带用户凭据,如果你在浏览器中访问了启用Basic认证的资源时,浏览就会弹出一个登录框,如图:
基本认证的基本过程:
- 1.客户端发送http请求给服务器,服务器验证该用户是否已经登录验证过了,如果没有的话, 服务器会返回一个401 Unauthozied给客户端,并且在响应header中添加一个 “WWW-Authenticate” 字段,例如:
WWW-Authenticate: Basic realm="admin"
其中"Basic"为认证方式,realm为用户角色的分组,可以在后台添加分组。
- 2.客户端得到响应码后,将用户名和密码进行base64编码(格式为用户名:密码),设置请求头Authorization,继续访问 :
Authorization: Basic YXXFISDJFISJFGIJIJG
服务器验证用户凭据,如果通过就返回资源内容。
注意: Http 的方式除了 Basic 认证之外还有:Digest 认证、Client 认证、Form Based 认证等,目前Flutter 的 HttpClient 只支持 Basic 和 Digest 两种认证方式,这两种认证方式最大的区别是发送用户凭据时,对于用户凭据的内容,前者只是简单的通过 Base64 编码(可逆),而后者会进行哈希运算,相对来说安全一点点,但是为了安全起见,无论是采用Basic认证还是Digest认证,都应该在Https协议下,这样可以防止抓包和中间人攻击。
4.1 HttpClient关于Http认证的方法和属性:
1. addCredentials(Uri url, String realm, HttpClientCredentials credentials)
该方法用于添加用户凭据,如:
httpClient.addCredentials(_uri,
"admin",
HttpClientBasicCredentials("username","password"), //Basic认证凭据
);
如果是Digest认证,可以创建Digest认证凭据:
HttpClientDigestCredentials("username","password")
2. authenticate(Future<bool> f(Uri url, String scheme, String realm))
这是一个setter,类型是一个回调,当服务器需要用户凭据且该用户凭据未被添加时,httpClient会调用此回调,在这个回调当中,一般会调用addCredential()来动态添加用户凭证,例如:
httpClient.authenticate=(Uri url, String scheme, String realm) async{
if(url.host=="xx.com" && realm=="admin"){
httpClient.addCredentials(url,
"admin",
HttpClientBasicCredentials("username","pwd"),
);
return true;
}
return false;
};
建议:如果所有请求都需要认证,那么应该在HttpClient初始化时就调用addCredentials()
来添加全局凭证,而不是去动态添加。
5. 代理
可以通过findProxy来设置代理策略,例如,我们要将所有请求通过代理服务器(192.168.1.2:8888)发送出去:
client.findProxy = (uri) {
// 如果需要过滤uri,可以手动判断
return "PROXY 192.168.1.2:8888";
};
findProxy 回调返回值是一个遵循浏览器PAC脚本格式的字符串,详情可以查看API文档,如果不需要代理,返回"DIRECT"即可。
在APP开发中,很多时候我们需要抓包来调试,而抓包软件(如charles)就是一个代理,这时我们就可以将请求发送到我们的抓包软件,我们就可以在抓包软件中看到请求的数据了。
有时代理服务器也启用了身份验证,这和http协议的认证是相似的,HttpClient提供了对应的Proxy认证方法和属性:
set authenticateProxy(
Future<bool> f(String host, int port, String scheme, String realm));
void addProxyCredentials(
String host, int port, String realm, HttpClientCredentials credentials);
他们的使用方法和上面“HTTP请求认证”中的addCredentials
和authenticate
相同,故不再赘述。
6. 证书校验
Https中为了防止通过伪造证书而发起的中间人攻击,客户端应该对自签名或非CA颁发的证书进行校验。HttpClient
对证书校验的逻辑如下:
- 如果请求的Https证书是可信CA颁发的,并且访问host包含在证书的domain列表中(或者符合通配规则)并且证书未过期,则验证通过。
- 如果第一步验证失败,但在创建HttpClient时,已经通过 SecurityContext 将证书添加到证书信任链中,那么当服务器返回的证书在信任链中的话,则验证通过。
- 如果1、2验证都失败了,如果用户提供了
badCertificateCallback
回调,则会调用它,如果回调返回true
,则允许继续链接,如果返回false
,则终止链接。
综上所述,我们的证书校验其实就是提供一个badCertificateCallback
回调,下面通过一个示例来说明。
示例
假设我们的后台服务使用的是自签名证书,证书格式是PEM格式,我们将证书的内容保存在本地字符串中,那么我们的校验逻辑如下:
String PEM="XXXXX";//可以从文件读取
...
httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
if(cert.pem==PEM){
return true; //证书一致,则允许发送数据
}
return false;
};
X509Certificate是证书的标准格式,包含了证书除私钥外所有信息。另外,上面的示例没有校验host,是因为只要服务器返回的证书内容和本地的保存一致就已经能证明是我们的服务器了(而不是中间人),host 验证通常是为了防止证书和域名不匹配。
对于自签名的证书,我们也可以将其添加到本地证书信任链中,这样证书验证时就会自动通过,而不会再走到badCertificateCallback回调中:
SecurityContext sc = SecurityContext();
//file为证书路径
sc.setTrustedCertificates(file);
//创建一个HttpClient
HttpClient httpClient = HttpClient(context: sc);
注意: 通过setTrustedCertificates()
设置的证书格式必须为 PEM 或 PKCS12,如果证书格式为PKCS12,则需将证书密码传入,这样则会在代码中暴露证书密码,所以客户端证书校验不建议使用 PKCS12 格式的证书。
总结:HttpClient提供的这些属性和方法最终都会作用在请求header里,我们完全可以通过手动去设置header来实现,之所以提供这些方法,只是为了方便开发者而已