我们在做应用开发的时候,经常需要利用一些工具来抓取网络请求接口,这样可以极大的方便接口联调。在使用 Flutter
做应用开发时就需要用到 Charles
这个抓包工具。
1. 抓包工具 Charles
我们到Charles官网下载:
1.1 Charles 破解
Charles 是收费软件,首次打开会提示你,可以免费试用30天,但是每次使用时间不能超过30分钟,并且启动时将会有10秒的延时。
打开Charles破解网站,输入你当前下载的 Charles 的版本号,点击生成,如下图显示:
这时候会跳出弹框,复制一下 License Key的值:
打开 Charles 工具后,点击 Help——Registered Name,跳出弹框输入版本号和 License Key 的值,点击 Register:
然后重启 Charles 即可。
1.2. Charles 抓包设置
1.2.1 配置 Charles 根证书
首先打开 Charles,help -> SSL Proxying -> install charles root certificate
之后会弹出钥匙串,如果没有弹出,请自行打开,如下图:
系统默认是不信任 Charles 的证书的,此时对不信任的证书右键,在弹出的下拉菜单中选择『显示简介』,点击使用此证书时,把使用系统默认改为始终信任,如下图:
然后关掉当前弹框系统会提示了输入密码,输入或者指纹验证后,出现下图所示即说明 charles 的证书已经被信任了:
还有钥匙串的系统的证书也需要同上述一样的操作选择信任:
1.2.2 Proxy Settings 代理设置
设置代理暴露端口:
1.2.3 SSL Proxying Settings(https url 抓包设置)
一般来说 配置 *.80
和 *.443
就可以了,但是有的可能不是443 直接配置 *
配置*号,即可以抓取ssl/https所有端口信息,然后已经可以抓取mac端的包了。
1.2.4 其他配置
- 如果你想过滤掉无关自身需求的站点,只想看单独某个站点,最简便的做法就是直接在 Charles 左下角过滤框中输入单独想看的站点请求:
如果你想看某几个站点的,按照下面操作:
然后,你就可以在列表中查看到添加过的网址,想要继续添加别的站点继续点击 Add 即可。
- 修改服务器返回内容,mock 模拟假数据,勾选Enable Map Local ,这样和后端调试的时候就不用等后端接口都完成才能调试了,只要有数据格式即可,调试的时候大有用处。对于一些平常测试不到的情况,异常情况等都可以随意模拟修改,这个功能很实用。
只需要本地创建一个.json文件,写 json 数据和接口关联起来即可。
1.3 移动端配置
我们查看Charles
的 help,按照下图所示来操作:
这里,我们可以看到一个弹框,告诉了你怎么给手机设置代理:
ps: 一定要让电脑和手机都在同一个局域网中(或是同一个WI-FI当中)
我这里以 iphone
为例,打开手机的设置,按照如下操作进行即可:
将上述 Charies 弹框中所列出的你的电脑的 ip 地址和端口号填写到下图:
配置完成后,Charles 会出现如下图所示弹窗,点击 Allow 允许即可:
然后打开手机上的safari浏览器
输入 chls.pro/ssl
网址,会下载一个 pem 格式的文件,要将其后缀 pem 改为 crt。如果你的手机浏览器无法安装,也可以在电脑上进入该网页下载安装,然后通过隔空投送到iphone上。
点击安装,开始认证:
然后回到手机设置页面,点击通用 ——> 关于本机 ——> 证书信任设置:
至此,Charles 就可以抓包移动端接口数据了。
2. 配置 Flutter 代理
完成工具准备后,由于 Flutter 默认不走系统代理
,所以我们还需要手动在 Flutter 项目中配置代理,这样 Charles 才能正确抓到包。
这里提供两种方案,一种是在请求库的配置里设置代理,另一种是利用 Flutter 原生的类来完成。
2.1 http 请求库配置代理
2.1.1 dio 配置代理
我们希望有一个设置代理的页面,让开发者在页面上输入代理。这里我构建了一个简单点的设置代理的UI页面:
class ProxySettingPage extends StatefulWidget {
const ProxySettingPage({Key? key}) : super(key: key);
@override
State<ProxySettingPage> createState() => _ProxySettingPageState();
}
class _ProxySettingPageState extends State<ProxySettingPage> {
late bool _isOn; // 是否开启代理
final TextEditingController _controller = TextEditingController(); // TextEditingController 实例,用于控制和获取 TextField 的内容
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomNavigatorBar(title: 'PROXY'),
body: Column(
children: [
TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: '172.168.0.1',
),
),
CupertinoSwitch( // switch开关组件,用户可以切换代理的开关状态。当状态改变时,调用 _action 方法更新设置。
value: _isOn,
onChanged: _action
)
],
),
);
}
}
初始化时,我们需要从 Cookies 中获取代理设置的状态和 IP 地址,并填入文本框:
@override
void initState() {
super.initState();
_isOn = Cookies.getBool('xx_proxy_on') ?? false;
_controller.text = Cookies.getString('xx_proxy_ip') ?? '';
}
接下来开始写 _action
方法,处理代理开关状态的变化:
-
根据
isOn
的值,更新 Cookies 中的代理状态和 IP 地址。 -
在 Flutter 项目中通常用
dio
库做 http 请求,我们可以通过 dio 的httpClientAdapter
属性配置我们的本地代理(DefaultHttpClientAdapter
提供了一个onHttpClientCreate
回调来设置底层HttpClient
的代理)。 -
更新
Http().dio
为新的Dio
实例,并设置自定义代理配置。
_action(bool isOn) {
_isOn = isOn;
Cookies.putBool('xx_proxy_on', isOn);
Cookies.putString('xx_proxy_ip', _controller.text);
FocusManager.instance.primaryFocus?.unfocus();
Dio oldDio = Http().dio; // 从 Http 服务中获取当前的 Dio 实例, 并将其保存到 oldDio 变量中
Dio dio = Dio(oldDio.options); // 通过 oldDio.options 创建一个新的 Dio 实例, 它继承了之前的 Dio 配置,如超时时间、请求头等
dio.interceptors.addAll(oldDio.interceptors); // 将旧 Dio 实例的所有拦截器添加到新的 Dio 实例中。这些拦截器可以用于请求和响应的拦截与处理,比如日志记录、错误处理
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) { // 将新的 dio 实例的 httpClientAdapter 强制转换为 DefaultHttpClientAdapter,并设置 onHttpClientCreate 回调。这允许我们对创建的 HttpClient 进行自定义配置
client.findProxy = (url) { // 在 findProxy 回调中,根据 isOn 的值决定是否启用代理
if (!isOn) { // 如果 isOn 为 false,返回 'DIRECT',表示直接连接,不使用代理
return 'DIRECT';
}
String ip = Cookies.getString('xx_proxy_ip') ?? '';
return 'PROXY $ip:8888'; // 如果 isOn 为 true,从 Cookies 中获取代理 IP 地址,并返回 `PROXY $ip:8888`,表示使用指定的代理服务器和端口
};
client.userAgent = null; // 清除 userAgent 设置。如果之前有自定义的用户代理字符串,这里将其设置为 null
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; // 设置一个回调函数来处理无效证书。在这里,所有的证书都被接受,无论其是否有效,这通常用于开发环境中测试,但不推荐在生产环境中使用
return client;
};
Http().dio = dio; // 更新全局 Dio 实例。以后所有通过 Http().dio 进行的请求都会使用新的代理设置
setState(() {});
}
其中client.findProxy
函数用来返回我们的代理接口,Charles 的默认的系统代理端口是8888
,所以这里配置成 PROXY $ip:8888
就可以了。
Flutter 请求 https 时用的是自己的CA认证证书,所以 Charles 在认证证书时会通不过证书校验,从而导致抓包错误。故而我们直接通过 client.badCertificateCallback
函数返回 true
来忽略证书就好了。
当然,在有些实际项目中,不会单独写个页面让开发者手填代理,而是将代理抽取到配置文件中,只要你在代码中手动添加/修改你的代理即可,如下图所示:
ps:一般情况下,开发者需要填写的代理都是个人电脑 ip 地址,我们除了可以通过在终端输入命令ipconfig
来查看 ip 外,还可以通过 Charles 来查看,如图所示:
2.1.2 web_socket_channel 配置代理
如果你的项目中需要抓取 websocket
, 那么在有了上述的 dio
配置后还不够,这时候我们的 websocketb 请求还是无法抓包,我们需要借助 web_socket_channel
这个库。
因为官方的 web_socket_channel
还不支持代理,所以我们不能直接从官网拉取这个库。我们可以改成以下方式进行安装:
dependencies:
web_socket_channel:
git:
url: https://github.com/IFreeOvO/web_socket_channel.git
ref: master
基于上述 dio 配置的思路,我们来开始配置 web_socket_channel
:
import 'package:web_socket_channel/io.dart';
// 创建一个自己的 HttpClient 对象
SecurityContext ctx = SecurityContext.defaultContext; // SecurityContext.defaultContext 返回默认的安全上下文 (SecurityContext) 实例。这个上下文用于配置与 SSL/TLS 相关的安全设置,比如证书验证
HttpClient client = HttpClient(context: ctx) // 创建一个 HttpClient 实例,并将之前获取的 SecurityContext (ctx) 传递给它。这使得 HttpClient 使用该上下文进行安全通信
..findProxy = ((url) { // 使用级联操作符 `..` 来设置 HttpClient 的 findProxy 属性
String ip = Cookies.getString('xx_proxy_ip') ?? '';
return 'PROXY $ip:8888';
})
..badCertificateCallback = (cert, host, port) { // 使用级联操作符 `..` 设置 HttpClient 的 badCertificateCallback
return true;
};
_channel = IOWebSocketChannel.connect( // 使用 IOWebSocketChannel 的 connect 方法创建一个 WebSocket 连接
'wss://xxx.com',
customClient: client, // 指定使用前面创建的自定义 HttpClient
);
2.2 重写原生方法
在入口文件main.dart
中,我们需要定义一个HttpOverrides
的子类,重写它的createHttpClient
方法。原理也是一样的,把findProxy
和badCertificateCallback
方法进行替换,然后挂载到全局。
// 重写 HttpOverrides
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
var http = super.createHttpClient(context);
http.findProxy = (url) {
return 'PROXY localhost:8888'; // 将 localhost 改成具体的 ip 地址
};
http.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return http;
}
}
void main() {
HttpOverrides.global = MyHttpOverrides(); // 应用中的所有 HTTP 请求都使用自定义的 HttpClient 设置
runApp(MyApp());
}
这种方案的好处是不受第三方请求库限制,配置完后https
和websocket
都能正确抓包。