Flutter 如何调用API获取数据及处理自签名证书报错、参数展示问题
Flutter自签名接口请求适配与响应数据展示实现
问题根源
你遇到的HandshakeException是因为目标接口使用自签名SSL证书,未被系统内置的可信CA列表收录,Flutter默认的TLS握手流程会直接拦截这类证书,触发证书校验失败报错。
注:http包底层同样基于HttpClient实现,之前出现的报错和直接使用HttpClient的报错原因完全一致,并非库本身的兼容问题。
规范的TLS握手适配方案
不要直接全局关闭证书校验(存在中间人攻击风险),按照以下步骤配置:
- 从目标服务端导出对应的自签名证书文件,存放到项目
assets/ssl/目录下,在pubspec.yaml中声明该资源路径 - 初始化网络客户端时,将该证书加入受信任列表,仅对目标IP的接口放行匹配的自签证书,不影响其他正常HTTPS接口的校验逻辑
- 配置示例代码:
import 'dart:io'; import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/services.dart' show rootBundle; Future<HttpClient> getConfiguredClient() async { // 加载本地存放的服务端自签证书 final certBytes = await rootBundle.load('assets/ssl/server_cert.crt'); final securityContext = SecurityContext(withTrustedRoots: true); securityContext.setTrustedCertificatesBytes(certBytes.buffer.asUint8List()); final client = HttpClient(context: securityContext); // 兜底校验逻辑:仅对目标IP放行匹配证书的请求 client.badCertificateCallback = (cert, host, port) { // 精准匹配目标接口IP,不放开其他请求的证书校验 if (host == '185.86.145.54') { // 校验证书主体信息,确认是目标服务的合法证书 return cert.subject.toString().contains('cameras_service'); } return false; }; return client; }
响应数据解析与UI展示流程
已经可以拿到HttpClient的响应流后,按照以下步骤完成数据渲染:
- 第一步:把响应流转换为结构化数据
- 读取响应流时,用utf8解码器将字节流转为字符串
- 通过
jsonDecode将字符串格式的JSON内容解析为Map/List结构的Dart对象,直接通过key就能取到接口返回的各参数值
解析示例:
Future<List<dynamic>> fetchCameras() async { final client = await getConfiguredClient(); final request = await client.getUrl(Uri.parse('https://185.86.145.54/cameras/all')); final response = await request.close(); // 转成字符串 final respStr = await response.transform(utf8.decoder).join(); // 解析JSON final respJson = jsonDecode(respStr) as Map<String, dynamic>; // 取data字段下的摄像机列表 return respJson['data'] as List<dynamic>; } - 第二步:将解析后的数据绑定到UI组件
- 推荐配合
FutureBuilder处理异步请求的全状态:加载中展示加载动画、请求失败展示错误提示、请求成功后遍历列表渲染内容 - 生产环境建议把返回的JSON对象映射为强类型的Model类,避免硬编码key带来的空值、字段拼写错误等问题
展示示例:
// 在页面的build方法中 FutureBuilder<List<dynamic>>( future: fetchCameras(), builder: (context, snapshot) { // 加载状态 if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } // 错误状态 if (snapshot.hasError) { return Center(child: Text('加载失败:${snapshot.error}')); } // 数据成功状态 final cameraList = snapshot.data ?? []; return ListView.builder( itemCount: cameraList.length, itemBuilder: (context, index) { final camera = cameraList[index]; return Card( child: ListTile( title: Text(camera['camera_name']), subtitle: Text(camera['is_online'] ? '当前在线' : '当前离线'), trailing: Text('ID: ${camera['camera_id']}'), ), ); }, ); }, ) - 推荐配合
注意事项
- 不要在
badCertificateCallback中直接写return true的逻辑,这种写法会让整个App的所有HTTPS请求都跳过证书校验,存在严重的安全风险 - 网络请求不要放在
build方法中执行,避免页面rebuild时重复触发无效请求 - 解析数据时要对所有可能为null的字段做空安全判断,防止运行时出现空指针异常
- 如果想切回http包使用,只需要把上面配置好的SecurityContext传给自定义的HttpClientAdapter,http包也能正常适配这个自签证书接口,不需要单独维护HttpClient的实例
内容的提问来源于stack exchange,提问作者Pation
相关产品推荐
相关产品推荐

