Flutter中能否使用Digest Authentication获取Onvif摄像头JPEG快照?
Flutter中通过Digest Authentication获取Onvif设备JPEG快照
当然可以实现,核心是处理Digest认证的双向握手流程——因为CachedNetworkImage默认只支持Basic Auth,所以需要自定义HTTP请求逻辑或扩展它的客户端来适配Digest认证。以下是两种可行方案:
方案1:手动处理Digest认证,直接获取图片字节流
使用http包手动完成认证握手,拿到图片字节后用Image.memory显示,适合不需要缓存的场景:
步骤1:依赖准备
确保在pubspec.yaml中添加依赖:
dependencies: http: ^1.1.0 crypto: ^3.0.3
步骤2:实现认证请求函数
import 'dart:convert'; import 'dart:typed_data'; import 'dart:math'; import 'package:http/http.dart' as http; import 'package:crypto/crypto.dart'; Future<Uint8List?> fetchOnvifSnapshot(String url, String username, String password) async { final client = http.Client(); try { // 1. 发送初始请求,触发认证挑战 var response = await client.get(Uri.parse(url)); if (response.statusCode == 200) { return response.bodyBytes; } else if (response.statusCode != 401) { throw Exception('请求失败:${response.statusCode}'); } // 2. 解析服务器返回的Digest认证头 final authHeader = response.headers['www-authenticate']!; final match = RegExp(r'digest realm="([^"]+)", nonce="([^"]+)"').firstMatch(authHeader); if (match == null) throw Exception('无法解析认证头'); final realm = match.group(1)!; final nonce = match.group(2)!; final uri = Uri.parse(url); final method = 'GET'; final qop = 'auth'; final nc = '00000001'; // 请求计数,首次为1,后续需递增 final cnonce = _generateCnonce(); // 3. 计算Digest响应所需的哈希值 final ha1 = md5.convert(utf8.encode('$username:$realm:$password')).toString(); final ha2 = md5.convert(utf8.encode('$method:${uri.path}')).toString(); final responseDigest = md5.convert(utf8.encode('$ha1:$nonce:$nc:$cnonce:$qop:$ha2')).toString(); // 4. 构造认证头并重新发送请求 final authResponse = 'Digest username="$username", realm="$realm", nonce="$nonce", uri="${uri.path}", qop=$qop, nc=$nc, cnonce="$cnonce", response="$responseDigest"'; response = await client.get(Uri.parse(url), headers: {'Authorization': authResponse}); if (response.statusCode == 200) { return response.bodyBytes; } else { throw Exception('认证失败:${response.statusCode}'); } } finally { client.close(); } } // 生成随机cnonce值 String _generateCnonce() { final random = Random.secure(); final bytes = Uint8List(16); for (var i = 0; i < 16; i++) { bytes[i] = random.nextInt(256); } return base64.encode(bytes); }
步骤3:在Widget中使用
FutureBuilder<Uint8List?>( future: fetchOnvifSnapshot('http://你的相机IP/onvif/snapshot', '用户名', '密码'), builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.waiting: return const CircularProgressIndicator(); case ConnectionState.done: if (snapshot.hasError) { return Text('加载失败:${snapshot.error}'); } if (snapshot.hasData && snapshot.data != null) { return Image.memory(snapshot.data!); } return const Text('无图片数据'); default: return const SizedBox.shrink(); } }, )
方案2:自定义CachedNetworkImage的HTTP客户端,支持Digest认证
如果需要图片缓存功能,可以封装一个支持Digest认证的HTTP客户端,传给CachedNetworkImage:
实现自定义客户端
class DigestAuthClient extends http.BaseClient { final http.Client _innerClient; final String username; final String password; int _requestCount = 1; // 用于递增nc值 DigestAuthClient(this._innerClient, {required this.username, required this.password}); @override Future<http.StreamedResponse> send(http.BaseRequest request) async { var response = await _innerClient.send(request); if (response.statusCode == 401) { final authHeader = response.headers['www-authenticate']; if (authHeader != null && authHeader.startsWith('digest')) { final match = RegExp(r'digest realm="([^"]+)", nonce="([^"]+)"').firstMatch(authHeader); if (match != null) { final realm = match.group(1)!; final nonce = match.group(2)!; final uri = request.url; final method = request.method; final qop = 'auth'; final nc = '${_requestCount.toString().padLeft(8, '0')}'; final cnonce = _generateCnonce(); final ha1 = md5.convert(utf8.encode('$username:$realm:$password')).toString(); final ha2 = md5.convert(utf8.encode('$method:${uri.path}')).toString(); final responseDigest = md5.convert(utf8.encode('$ha1:$nonce:$nc:$cnonce:$qop:$ha2')).toString(); final authResponse = 'Digest username="$username", realm="$realm", nonce="$nonce", uri="${uri.path}", qop=$qop, nc=$nc, cnonce="$cnonce", response="$responseDigest"'; request.headers['Authorization'] = authResponse; _requestCount++; response = await _innerClient.send(request); } } } return response; } String _generateCnonce() { final random = Random.secure(); final bytes = Uint8List(16); for (var i = 0; i < 16; i++) { bytes[i] = random.nextInt(256); } return base64.encode(bytes); } }
步骤2:在CachedNetworkImage中使用
CachedNetworkImage( imageUrl: 'http://你的相机IP/onvif/snapshot', httpClient: DigestAuthClient(http.Client(), username: '用户名', password: '密码'), placeholder: (context, url) => const CircularProgressIndicator(), errorWidget: (context, url, error) => const Icon(Icons.error_outline), )
注意事项
- 不同Onvif设备的快照URL可能存在差异,常见路径有
/snapshot、/onvif/media/snapshot,需根据设备文档确认 - 若需多次请求,建议维护
nc(请求计数)的递增逻辑,避免认证失败 - 可添加异常捕获逻辑,处理网络断开、设备离线等场景
内容的提问来源于stack exchange,提问作者TheDizzyEgg
相关产品推荐
相关产品推荐

