Files
hjsq_android/analytics_sdk/lib/utils/device_fingerprint_util.dart
T
2026-10-08 11:37:25 +07:00

217 lines
7.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}