Flutter-Http请求

554 阅读7分钟

Dart IO库中提供了用于发起Http请求的一些类,可以直接使用HttpClient来发起请求。使用步骤分五步:

  1. 创建HttpClient
HttpClient httpClient = HttpClient();
  1. 打开Http连接,设置请求头
HttpClientRequest request = await httpClient.getUrl(uri);

这一步可以使用任意Http Method,如httpClient.post()、http.delete()等。
如包含Query参数,可以在构建uri时添加:

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

通过HttpClientRequest可以设置请求的header:

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

如果是post或put等可以携带请求体方法,可以通过HttpClientRequest对象发送请求

String payload="...";
request.add(utf8.encode(payload)); 
//request.addStream(_inputStream); //可以直接添加输入流
  1. 等待连接服务器
HttpClientResponse response = await request.close();

这一步完成后,请求信息就已经发送到服务器,返回一个HttpClientResponse对象,它包含响应头(header)和响应流(响应体的Stream),接下来就可以通过读取响应流来获取响应内容。 4. 读取响应内容

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

通过读取响应流来获取服务器返回的数据,在读取时,可以设置编码格式,如utf8. 5. 请求结束,关闭HttpClient

httpClient.close();

关闭client后,通过client发起的所有请求都会终止。

示例:

import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';

class HttpTestRoute extends StatefulWidget{
  const HttpTestRoute({Key? key}):super(key: key);
  
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return HttpTestRouteState();
  }
}

class HttpTestRouteState extends State<HttpTestRoute>{
  bool loading = false;
  String textData = "";
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: const Text("SSL Http Test"),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            ElevatedButton(
                onPressed: (){
                  if (!loading) {
                    requestData();
                  }
                },
                child: const Text("Get BaiDu Data")
            ),
            Container(
              width: MediaQuery.of(context).size.width - 50,
              child: Text(textData.replaceAll(RegExp(r"\s"), "")),
            )
          ],
        ),
      ),
    );
  }
  
  requestData() async{
    setState(() {
      loading = true;
      textData = "正在请求。。。";
    });
    try {
      //创建一个HttpClient
      HttpClient httpClient = 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");
      HttpClientResponse response = await request.close();

      textData = await response.transform(utf8.decoder).join();
      debugPrint("${response.headers}");
      httpClient.close();
    } catch (e){
      textData = "请求失败 $e";
    }finally{
      setState(() {
        loading = false;
      });
    }

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设置的只对当前的请求生效。

HTTP请求认证

Http协议的认证(Authertication)机制可以用于保护非公开资源,如果Http服务器开启了认证,那么用户在发起请求时,就需要携带用户凭据,如果在浏览器中访问了启用Basic认证的资源时,浏览就会弹出一个登录框。
Basic认证的基本过程:

  1. 客户端发送http请求给服务器,服务器验证该用户是否已经登录验证过,如果没有,则服务器会返回401Unauthozied给客户端,并且在响应header中添加一个“WWW-Autherticate”字段:
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协议下,这样可以防止抓包和中间人攻击。

HttpClient关于Http认证的方法和属性:

  1. addCredentials(Uri url, String realm, HttpClientCredential credentials)
    该方法用于添加用户凭据,如:
httpClient.addCredentials(_uri,
 "admin", 
  HttpClientBasicCredentials("username","password"), //Basic认证凭据
);

如果是Digest认证,可以创建Digest认证凭据:

HttpClientDigestCredentials("username","password")
  1. 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(),来添加全局凭证,而不是去动态添加。

代理

可以通过findPrixy来设置代理策略,例如,要将所有请求通过代理服务器(192.168.1.1:9999):

client.findProxy = (uri) {
    // 如果需要过滤uri,可以手动判断
    return "PROXY 192.168.1.1:9999";
 };

findProxy回调返回值是一个遵循浏览器PAC脚本格式的字符串,如果不需要代理,返回DIRECT接口。

在开发中,很多时候需要抓包来调试,而抓包软件如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相同。

证书校验

Https中为了防止通过伪造证书而发起的中间人攻击,客户端应该对自己签名或非CA颁发的证书进行校验。HttpClient对证书校验的逻辑如下:

  1. 如果请求的Https证书时可信CA颁发的,并且访问host包含在证书的domain列表中(或者符合通配规则)并且证书未过期,则验证通过。
  2. 如果第一步验证失败,但在创建HttpClient时,已经通过SecurityContext将证书添加到证书信任链中,那么当服务器返回的证书在信任链中的话,则验证通过。
  3. 如果1、2验证都失败了,如果用户提供了badCertificateCallback回调,则会调用它,如果回调返回true,则允许继续链接,如果返回false,则终止链接。

综上所述,证书校验起始就是提供一个badCertificateCallback回调。
示例:

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格式的证书。