217 lines
7.3 KiB
Dart
217 lines
7.3 KiB
Dart
import 'device_fingerprint_native.dart'
|
||
// ignore: uri_does_not_exist
|
||
if (dart.library.html) 'device_fingerprint_web.dart';
|
||
|
||
import 'dart:convert';
|
||
|
||
import 'package:crypto/crypto.dart';
|
||
|
||
/// 设备指纹工具类
|
||
///
|
||
/// SDK 内部自动调用,业务方无需手动传入。
|
||
///
|
||
/// **生成规则**
|
||
/// 1. 现行外层 `device_fingerprint` 使用 v3 规则:
|
||
/// 基于 `sdk_init` payload 内的平台稳定字段,按固定顺序规范化后做
|
||
/// SHA-256,输出 64 位小写十六进制。
|
||
/// 2. 网络、电量、页面、行为等易变字段不参与 v3 指纹。
|
||
/// 3. [getFingerprint] 保留为旧调用方兼容入口;SDK 自动上报链路使用
|
||
/// [generateV3FromSdkInitPayload]。
|
||
///
|
||
/// **平台差异**
|
||
/// - Native(Android/iOS/桌面):降级 UUID 存于 `applicationDocumentsDirectory/analytics_fingerprint.txt`
|
||
/// - Web:降级 UUID 存于 `localStorage['analytics_fp']`
|
||
///
|
||
/// **失败兜底**:任何异常均返回空字符串,事件正常上报,仅 `device_fingerprint` 字段为空。
|
||
///
|
||
/// **注意**:`device_fingerprint` 不参与 `event_id` 的计算。
|
||
class DeviceFingerprintUtil {
|
||
/// 指纹生成规则版本号。
|
||
///
|
||
/// 每次生成规则(因子组合、哈希算法)发生变更时,此版本号必须同步递增,
|
||
/// 以便服务端区分不同规则下生成的指纹,支持跨版本数据对比分析。
|
||
///
|
||
/// 版本历史:
|
||
/// - `3.0.0`:按 sdk_init 启动快照中的平台稳定字段生成。
|
||
static const String kVersion = '3.0.0';
|
||
|
||
static String? _cache;
|
||
|
||
/// 获取设备指纹。
|
||
///
|
||
/// 首次调用时根据传入的设备因子计算并内存缓存,后续直接返回缓存值。
|
||
/// 若设备因子发生变化需重新计算,请先调用 [clearCache]。
|
||
static Future<String> getFingerprint({
|
||
String deviceId = '',
|
||
String device = '',
|
||
String deviceBrand = '',
|
||
String deviceModel = '',
|
||
String systemName = '',
|
||
String systemVersion = '',
|
||
String userAgent = '',
|
||
}) async {
|
||
if (_cache != null) return _cache!;
|
||
_cache = await generateOrLoadFingerprint(
|
||
deviceId: deviceId,
|
||
device: device,
|
||
deviceBrand: deviceBrand,
|
||
deviceModel: deviceModel,
|
||
systemName: systemName,
|
||
systemVersion: systemVersion,
|
||
userAgent: userAgent,
|
||
);
|
||
return _cache!;
|
||
}
|
||
|
||
/// 清除内存缓存(通常仅测试时使用,不会删除本地存储文件)
|
||
static void clearCache() {
|
||
_cache = null;
|
||
}
|
||
|
||
/// 按产品文档 v3 规则,基于 sdk_init payload 生成外层设备指纹。
|
||
///
|
||
/// 该方法只使用启动快照内相对稳定的字段;网络、电量、页面、行为等
|
||
/// 易变字段不参与。输入缺失时保留字段名并使用文档默认值,确保同一
|
||
/// 平台的参与字段顺序稳定。
|
||
static String generateV3FromSdkInitPayload(Map<String, dynamic> payload) {
|
||
try {
|
||
final platform = _fingerprintPlatform(payload);
|
||
final values = platform == 'web'
|
||
? _webFingerprintValues(payload)
|
||
: platform == 'ios'
|
||
? _iosFingerprintValues(payload)
|
||
: _androidFingerprintValues(payload);
|
||
final canonical =
|
||
'fp_v3|fingerprint_platform=$platform|${values.join('|')}';
|
||
return sha256.convert(utf8.encode(canonical)).toString();
|
||
} catch (_) {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
static String _fingerprintPlatform(Map<String, dynamic> payload) {
|
||
if (_string(payload['platform']) == 'web') return 'web';
|
||
final osFamily = _string(payload['os_family']);
|
||
if (osFamily == 'ios') return 'ios';
|
||
return 'android';
|
||
}
|
||
|
||
static List<String> _androidFingerprintValues(Map<String, dynamic> payload) {
|
||
final id = _selectFirstId(payload, const [
|
||
'oaid',
|
||
'aaid',
|
||
'android_id',
|
||
'guid',
|
||
]);
|
||
return [
|
||
_kvString(payload, 'os_family'),
|
||
_kvString(payload, 'brand'),
|
||
_kvString(payload, 'manufacturer'),
|
||
_kvString(payload, 'model'),
|
||
_kvString(payload, 'abi'),
|
||
_kvInt(payload, 'screen_width'),
|
||
_kvInt(payload, 'screen_height'),
|
||
_kvInt(payload, 'screen_dpi'),
|
||
_kvInt(payload, 'device_pixel_ratio'),
|
||
_kvInt(payload, 'cpu_cores'),
|
||
_kvInt(payload, 'device_memory_gb'),
|
||
_kvString(payload, 'build_hardware'),
|
||
_kvString(payload, 'build_product'),
|
||
_kvString(payload, 'build_fingerprint'),
|
||
_kvInt(payload, 'has_baseband'),
|
||
_kvString(payload, 'language'),
|
||
_kvString(payload, 'timezone'),
|
||
'id_source=${id.source}',
|
||
'id_value=${id.value}',
|
||
];
|
||
}
|
||
|
||
static List<String> _iosFingerprintValues(Map<String, dynamic> payload) {
|
||
final idfv = _string(payload['idfv']);
|
||
return [
|
||
_kvString(payload, 'os_family'),
|
||
_kvString(payload, 'ios_device_model_raw'),
|
||
_kvInt(payload, 'screen_width'),
|
||
_kvInt(payload, 'screen_height'),
|
||
_kvInt(payload, 'screen_dpi'),
|
||
_kvInt(payload, 'device_pixel_ratio'),
|
||
_kvInt(payload, 'cpu_cores'),
|
||
_kvInt(payload, 'device_memory_gb'),
|
||
_kvString(payload, 'language'),
|
||
_kvString(payload, 'timezone'),
|
||
'id_source=${idfv.isEmpty ? 'none' : 'idfv'}',
|
||
'id_value=$idfv',
|
||
];
|
||
}
|
||
|
||
static List<String> _webFingerprintValues(Map<String, dynamic> payload) {
|
||
return [
|
||
_kvString(payload, 'os_family'),
|
||
_kvString(payload, 'browser_kernel'),
|
||
_kvString(payload, 'browser_platform'),
|
||
_kvInt(payload, 'screen_width'),
|
||
_kvInt(payload, 'screen_height'),
|
||
_kvInt(payload, 'screen_dpi'),
|
||
_kvInt(payload, 'device_pixel_ratio'),
|
||
_kvInt(payload, 'color_depth'),
|
||
_kvInt(payload, 'max_touch_points'),
|
||
_kvInt(payload, 'hardware_concurrency'),
|
||
_kvString(payload, 'language'),
|
||
_kvString(payload, 'timezone'),
|
||
_kvInt(payload, 'cookie_enabled'),
|
||
_kvInt(payload, 'local_storage_enabled'),
|
||
_kvInt(payload, 'session_storage_enabled'),
|
||
_kvInt(payload, 'indexeddb_enabled'),
|
||
_kvInt(payload, 'service_worker_enabled'),
|
||
_kvString(payload, 'canvas_fp'),
|
||
_kvString(payload, 'webgl_fp'),
|
||
_kvString(payload, 'audio_fp'),
|
||
_kvString(payload, 'fonts_hash'),
|
||
_kvString(payload, 'plugins_hash'),
|
||
_kvString(payload, 'mimes_hash'),
|
||
];
|
||
}
|
||
|
||
static _SelectedId _selectFirstId(
|
||
Map<String, dynamic> payload,
|
||
List<String> keys,
|
||
) {
|
||
for (final key in keys) {
|
||
final value = _string(payload[key]);
|
||
if (value.isNotEmpty) return _SelectedId(key, value);
|
||
}
|
||
return const _SelectedId('none', '');
|
||
}
|
||
|
||
static String _kvString(Map<String, dynamic> payload, String key) =>
|
||
'$key=${_string(payload[key])}';
|
||
|
||
static String _kvInt(Map<String, dynamic> payload, String key) =>
|
||
'$key=${_int(payload[key])}';
|
||
|
||
static String _string(dynamic value) {
|
||
if (value == null) return '';
|
||
return value.toString().trim().toLowerCase();
|
||
}
|
||
|
||
static int _int(dynamic value) {
|
||
if (value is int) return value;
|
||
if (value is double && value.isFinite) return value.round();
|
||
if (value is String) {
|
||
final text = value.trim();
|
||
final asInt = int.tryParse(text);
|
||
if (asInt != null) return asInt;
|
||
final asDouble = double.tryParse(text);
|
||
if (asDouble != null && asDouble.isFinite) return asDouble.round();
|
||
}
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
class _SelectedId {
|
||
final String source;
|
||
final String value;
|
||
|
||
const _SelectedId(this.source, this.value);
|
||
}
|