一种混合式设备指纹实现方案
一种混合式设备指纹实现方案
现在生成设备指纹的方式一般有三种,第一种为主动式,主动采集设备N多信息,如UA、MAC、Serial等,在客户端生成唯一识别码。第二种为被动式,在终端设备与服务器通信过程中,从数据报文的OSI七层协议中,提取该终端设备的OS、协议栈和网络状态相关的特征集,并结合机器学习算法以标识和跟踪具体的终端设备。今天想跟大家分享的就是第三种,混合式,既有主动采集的部分,又有服务端算法生成部分。通过植入SDK和JS,埋点在固定的业务场景,被动触发时的主动去采集要素,并与服务端交互,通过算法混淆加密后,在服务端生成唯一的设备指纹识ID,同时写入唯一ID存于app应用缓存或浏览器cookie中。一定时间内,用户再次使用对应业务埋点页面时,无需大量重新上传采集要素,只需比对要素变化比例,通过加权比对,计算得出置信度数值,并通过阈值判断是否重新生成设备指纹码。正常用户在使用时理论上是无感知且很少会主动篡改设备指纹唯一ID。
一、设备指纹流程
二、客户端主动采集部分
收集信息前有两个大前提,第一个就是Android系统版本越高,权限越收紧,标识设备的信息越难获取,如:Android8.0之后,序列号的获取跟IMEI权限绑定,如果不授权电话权限,同样获取不到序列号;Android 10.0之后,序列号、IMEI 非系统APP获取不到;Android 11.0之后,序列号、IMEI MAC 非系统APP获取不到。第二个就是隐私合规的要求,随着隐私合规被重视,监管机构也有相应要求,我们收集信息应该尽量避免能够直接标识设备、用户的信息。综上,这个设备指纹方案,不将IMEI、IMSI、Serial、网卡Mac归入采集字段。
下面给出采集的字段
先是一些设备的基本信息:
1、Build.MANUFACTURER 硬件制造商
2、Build.BRAND 手机品牌
3、Build.CPU_ABI cpu指令集
4、Build.DEVICE 设备参数
5、Build.DISPLAY 显示屏参数
6、Build.HOST host值
7、Build.ID 修订版本列表
8、Build.MODEL 设备版本
9、Build.PRODUCT 设备产品名
10、Build.TAGS 描述build的标签
11、Build.TYPE build类型
12、Build.USER 设备用户名
13、Build.VERSION.RELEASE 版本字符串
14、Build.FINGERPRINT 唯一编号
以上信息都可以通过 android.os.Build 来获取,参考代码如下:
public static String getPhoneManufacturer(){
Log.d("CJ","MANUFACTURER: " + Build.MANUFACTURER);
return Build.MANUFACTURER;
}
还有一些其他的基本信息
15、screen 屏幕大小 分辨率
dmint screenWidth = dm.widthPixels; int screenHeight = dm.heightPixels;
16、手机屏幕亮度
static int getScreenBrightness(Activity activity){int value = 0; ContentResolver cr = activity.getContentResolver(); try { value = Settings.System.getInt(cr,Settings.System.SCREEN_BRIGHTNESS); }catch (Exception e){ e.printStackTrace(); } Log.d("CJ","屏幕亮度:" + value); return value; }
17、sys.usb.state 系统USB状态
public static String getMtpMode(){
Class classType = null;
String function = null;
try {
classType = Class.forName("android.os.SystemProperties");
Method getMethod = classType.getDeclaredMethod("get",new Class[]{String.class});
function = (String)getMethod.invoke(classType,new Object[]{"sys.usb.state"});
Log.d("CJ","sys.usb.state:" + function);
}catch (Exception e){
e.printStackTrace();
}
return function;
}
18、gsm.network.type
public static int getNetworkType(Context context){
int networkType = IntenetUtil.getNetworkState(context);
Log.d("CJ","移动网络类型:" + networkType);
return networkType;
}
19、wifi下 周边wifi信息列表
public static List getScanWifiList(Context context){
WifiManager wifiManager = (WifiManager) context.getSystemService(WIFI_SERVICE);
ArrayList<ScanResult> list = (ArrayList<ScanResult>) wifiManager.getScanResults();
List ssid = new ArrayList<>();
for (int i = 0; i < list.size();i++){
ssid.add(list.get(i).SSID);
}
Log.d("CJ","wifiList: " + ssid);
return ssid;
}
20、cpu相关信息,cpu核数、cpu频率(最小频率 最大频率)
public static int getNumberOfCPUCores(){
int cores;
try {
cores = new File("/sys/devices/system/cpu/").listFiles(CPU_FILTER).length;
}catch (SecurityException e){
cores = 0;
}catch (NullPointerException e){
cores = 0;
}
Log.d("CJ","CPUCores: " + cores);
return cores;
}
最大频率public static long getCPUMinFreq(){ long result = 0L; try { String line; BufferedReader br = new BufferedReader(new FileReader("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq")); if ((line = br.readLine()) != null){ result = Long.parseLong(line); } br.close(); }catch (Exception e){ e.printStackTrace(); } Log.d("CJ","CPU最小频率:" + result); return result; } public static long getCPUMaxFreq(){ long result = 0L; try { String line; BufferedReader br = new BufferedReader(new FileReader("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")); if ((line = br.readLine()) != null){ result = Long.parseLong(line); } br.close(); }catch (Exception e){ e.printStackTrace(); } Log.d("CJ","CPU最大频率:" + result); return result; }
21、gpu相关信息,gpu渲染器、gpu供应商、gpu版本、gpu扩展名
public void onSurfaceChanged(GL10 gl,int arg1,int arg2){
glRenderer = gl.glGetString(GL10.GL_RENDERER);
glVendor = gl.glGetString(GL10.GL_VENDOR);
glVersion = gl.glGetString(GL10.GL_VERSION);
glExtensions = gl.glGetString(GL10.GL_EXTENSIONS);
}
22、freeSpace 剩余空间
freeSpace()public static long getFreeSpace(){ long phoneFreeSpace = Environment.getDataDirectory().getFreeSpace(); Log.d("CJ","phoneFreeSpace: " + phoneFreeSpace); return phoneFreeSpace; } //获取 SDfreeSpace() public static long getSDFreeSpace(){ long SDFreeSpace = Environment.getExternalStorageDirectory().getFreeSpace(); Log.d("CJ","SDFreeSpace: " + SDFreeSpace); return SDFreeSpace; }
23、availableSpace 可用空间
availableSpacepublic static long getAvailableRom(){ File path = Environment.getDataDirectory(); StatFs stat = new StatFs(path.getPath()); long blockSize = stat.getBlockSizeLong(); long availableBlocks = stat.getAvailableBlocksLong(); Log.d("CJ","blockSize: " + blockSize +"; availableBlocks: " + availableBlocks + "; " + "result: " + (blockSize * availableBlocks/1000000) + ";"); return blockSize * availableBlocks/1000000; }
24、设备开机时间
public static String getSystemStartupTime(){
long time = System.currentTimeMillis() - SystemClock.elapsedRealtime();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d1 = new Date(time);
Log.d("CJ","系统开机时间:" + format.format(d1));
return format.format(d1);
}
25、设备出厂时间 (能否获取和设备型号有较大关系)
public static String getFactoryTime(){
StringBuilder result = new StringBuilder();
ProcessBuilder cmd;
try {
String[] args = {"/system/bin/stat","/cache"};
cmd = new ProcessBuilder(args);
Process process = cmd.start();
InputStream in = process.getInputStream();
byte[] re = new byte[24];
while (in.read(re) != -1){
result.append(new String(re));
}
in.close();
}catch (Exception e){
e.printStackTrace();
}
String[] lines = result.toString().split("\n");
StringBuilder time = new StringBuilder();
for (String line:lines){
if (line.contains("Access:") && !line.contains("Uid:")){
for (char s: line.toCharArray()){
if (Character.isDigit(s)){
time.append(s);
}
}
}
}
Log.d("CJ","出厂时间:" + time);
return time.toString().equals("19700101080000000000000") ? "" : time.toString().replace("000000000", "");
}
26、accessibilityServiceInfo.getSettingsActivityName() (开启辅助模式的applist 或者activity)
static List getSettingActivityName(Context context){List<AccessibilityServiceInfo> enabledAccessibilityServiceList; List accessibilityActivity = new ArrayList<>(); AccessibilityManager accessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); if (accessibilityManager == null || !accessibilityManager.isEnabled() || (enabledAccessibilityServiceList = accessibilityManager.getEnabledAccessibilityServiceList(1)) == null){ return null; } for (AccessibilityServiceInfo accessibilityServiceInfo : enabledAccessibilityServiceList){ String settingsActivityName = accessibilityServiceInfo.getSettingsActivityName(); accessibilityActivity.add(settingsActivityName); } Log.d("CJ","accessibilityActivity: " + accessibilityActivity); return accessibilityActivity; }
以上基础设备信息也可以提供一些防篡改能力,比如Build.FINGERPRINT中也包含手机品牌、制造商等信息,如果和单独获取的对不上,可以判断有篡改风险;USB状态也可以判断是否为adb调试状态;wifi信息列表,可以判断设备聚集度,如果是群控设备,周边wifi列表大概率是一致的;还有些cpu信息,gpu信息、屏幕分辨率也可以和市面上的设备进行匹配,发现信息对不上,也可以作为改机的判断,但是这个可能需要自己先收集市面上设备的信息,建立一个设备库;开启辅助模式的包名可以判断设备上有无app开启辅助模式,去做一些模拟点击的操作。
下面是我认为用于设备唯一性判断较为重要的字段
1、mediaDrmId (deviceUniqueId) 数字产权管理(DRM)设备 ID。会根据底层包名不同而不同
public static String getDrmId2()throws Exception{
UUID uuid = new UUID(0xEDEF8BA979D64ACEL,0xA3C827DCD51D21EDL);
MediaDrm mediaDrm = new MediaDrm(uuid);
byte[] bytes = mediaDrm.getPropertyByteArray("deviceUniqueId");
String result = Base64.encodeToString(bytes,2);
Log.d("CJ","MediaDrmId: " + result);
return result;
}
2、Settings.Secure.ANDROID_ID Androidid 会根据底层包名不同而不同
除了androidid这个字段,ad_aaid、key_mqs_uuid、gcbooster_uuid 也可参与唯一性判断,但是这些字段部分型号手机可能没有。
public static String getAndroidId(Context context){
String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID );
Log.d("CJ","androidId1: " + androidId);
return androidId;
}
public static String getAndroidId2(Context context){
String androidId = Settings.Global.getString(context.getContentResolver(), "ad_aaid");
Log.d("CJ","androidId2: " + androidId);
return androidId;
}
public static String getAndroidId3(Context context){
String androidId = Settings.Global.getString(context.getContentResolver(), "key_mqs_uuid");
Log.d("CJ","androidId3: " + androidId);
return androidId;
}
public static String getAndroidId4(Context context){
String androidId = Settings.Global.getString(context.getContentResolver(), "gcbooster_uuid");
Log.d("CJ","androidId4: " + androidId);
return androidId;
}
这两个参数都有个问题,会根据底层包名不同而不同。如果一个公司旗下有多款app,想不同app在同个设备上生成同样的设备指纹,那这两个字段可能不能参与设备指纹的生成。
3、相册创建时间、上一次修改时间
static JSONObject getAlbumCreateTime()throws Exception{File file = new File("/sdcard" + "/" + Environment.DIRECTORY_DCIM + "/"); JSONObject albumInfo = new JSONObject(); if (file.isDirectory()){ File[] files = file.listFiles(); for(int i = 0;i < files.length;i++){ if (files[i].isDirectory()){ // Calendar cal = Calendar.getInstance(); long time = files[i].lastModified(); // cal.setTimeInMillis(time); albumInfo.put(files[i].getAbsolutePath(),time); } } } Log.d("CJ","albumInfo: " + albumInfo); return albumInfo; }
4、系统根目录文件 libc.so、libandroid_runtime.so创建时间
static long getLibcCreateTime()throws Exception{long creationTime = 0L; File file = new File("/system/lib/libc.so"); // File file = new File("/sdcard/DCIM/Camera"); BasicFileAttributes attr = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ attr = Files.readAttributes(Paths.get(file.getAbsolutePath()),BasicFileAttributes.class); FileTime createAt = attr.lastAccessTime(); if (createAt != null){ creationTime = createAt.toMillis(); } }else { Log.d("CJ","libcCreateTime无法获取"); } Log.d("CJ","libcCreateTime: " + creationTime); return creationTime; } public static long getLibAndroidRuntime()throws Exception{ File file = new File("/system/lib/libandroid_runtime.so"); long time = file.lastModified(); Log.d("CJ","libAndroidRuntime: " + time); return time; }
Android8以上, creationTime() 实测为上次修改时间 ,lastAccessTime() 上次访问时间,实测为创建时间, lastModifiedTime() 为上次修改时间。考虑Android版本,建议Native层获取。
5、popen获取文件系统id Blocks Inodes (stat -f /)
这边给出一个Native层,popen执行shell命令去获取的方式
cmd// std::string cmd = "cat /sys/devices/soc0/serial_number"; FILE *fp = ::popen(cmd.c_str(),"r"); if (!fp){ LOGI("open cmd<%s> failed. errno:%d, %s",cmd.c_str(),errno,strerror(errno)); return (jstring) ""; } std::string strResult; char szbuffer[1024 * 10]{}; while (!::feof(fp)){ size_t iCnt = ::fread(szbuffer,1, sizeof(szbuffer),fp); if (iCnt > 0){ strResult.append(szbuffer,szbuffer + iCnt); } } ::pclose(fp);
6、f_fsid 文件系统id(标识) 通过statfs结构体获取
statfs buf;if (statfs("/",&buf) == -1){ printf("statfs bad \n"); // exit(1); } printf("fsid = %d %d\n",buf.f_fsid.__val[0],buf.f_fsid.__val[1]); int fsid = buf.f_fsid.__val[0]; 7、boot_id Native层openat读取文件/proc/sys/kernel/random/boot_id void openatGetBootId(){ char buffer[256]; memset(buffer,0,256); std::string result; long fd = syscall(__NR_openat,dirfd,"/proc/sys/kernel/random/boot_id",O_RDONLY,O_RDONLY); while (read(fd,buffer,1) !=0 ){ result.append(buffer); } syscall(__NR_close,fd); LOGD("boot_id: = %s",result.c_str()); }
8、uuid Native层openat读取文件/proc/sys/kernel/random/uuid
openatGetUUID(){char buffer[256]; memset(buffer,0,256); std::string result; long fd = syscall(__NR_openat,dirfd,"/proc/sys/kernel/random/uuid",O_RDONLY,O_RDONLY); while (read(fd,buffer,1) !=0 ){ result.append(buffer); } syscall(__NR_close,fd); LOGD("uuid: = %s",result.c_str()); }
9、读取 /proc/net/arp 文件(arp缓存,内含ip地址 HW address)
10、/system/fonts 字体目录下文件名list的md5
"C"JNIEXPORT jstring JNICALL Java_com_xxx_aartest_GetInfoFromNative_listFileMd5FromJNI(JNIEnv *env, jobject thiz) { // TODO: implement listFileFrimJNI() std::vector<std::string> files; std::string strResult; // /system/fonts、/vendor/lib、/vendor/firmware、/system/framework、/system/bin List("/system/fonts",0,files); for(std::vector<std::string>::iterator iterator = files.begin();iterator!=files.end();++iterator){ // LOGD("file name =%s",(*iterator).c_str()); strResult.append((*iterator).c_str()); } // LOGD("file name =%s",strResult.c_str()); MD5 md5string = MD5(strResult); return env->NewStringUTF(md5string.toStr().c_str()); }
11、/vendor/lib 目录下文件名list的md5
12、/vendor/firmware 目录下文件名list的md5
13、/system/framework 目录下文件名list的md5
14、/system/bin 目录下文件名list的md5
这些字段就算同型号的设备也大概率不相同,能够很好的标识设备的唯一性。众多商用设备指纹也都会采集以上字段做判断。获取方式大多是读取文件,建议走svc内联汇编或者popen执行shell命令的方式获取。这样能够提高篡改成本。
三、服务端算法生成部分
首先是设备指纹生成逻辑,设备指纹一般是放在请求头或者请求体中传给后端的,如果采用AES、RSA等加密算法,在收集数据量较多时候,加密结果可能较长,所以尽量轻量化一点,采用md5、sha256等hash算法较为合适。先看网上一种uniquePsuedoId的设计方式:
static String getUniquePsuedoID(){String m_szDevIDShort = "35" + Build.BOARD.length() % 10 + Build.BRAND.length() % 10 + Build.CPU_ABI.length() % 10 + Build.DEVICE.length() %10 + Build.DISPLAY.length() % 10 + Build.HOST.length() %10 + Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 + Build.MODEL.length() % 10 + Build.PRODUCT.length() %10 + Build.TAGS.length() %10 + Build.TYPE.length() %10 + Build.USER.length() %10; //13位 Log.d("CJ","m_szDevIDShort: " + m_szDevIDShort); return m_szDevIDShort; }
这种方式是把每个参数的长度取余,然后拼接成字符串。虽然说重码率可能较低,但是如果各项参数的长度不变,那生成的指纹就是相同,这个逻辑还是不太可靠的。这边建议就拿上面收集的重要参数加盐来个MD5就美美哒···
然后是设备指纹相似算法的逻辑,当攻击者篡改了部分参数,我们如何能够识别出来,并且返回之前已有的设备指纹。这就需要对用户传过来的设备信息做相似性匹配,超过阈值就认为是一个新的设备,生成一个新的设备指纹,没有超过阈值则认为是已知设备,返回相似度较高那条信息的设备指纹。
下面给出几种相似性算法的demo,首先是余弦相似度:
public static double getCosAlgorithmResult(String str1,String str2){
StopRecognition filter = new StopRecognition();
//过滤标点
filter.insertStopNatures("w");
//分词-统计词频
Map<String,Integer> map1 = new HashMap<>();
ToAnalysis.parse(str1).recognition(filter).forEach(item -> {
//没有则赋初始值,有则+1
if (map1.get(item.getName()) == null){
map1.put(item.getName(),1);
}else {
map1.put(item.getName(),map1.get(item.getName()) + 1);
}
});
Map<String,Integer> map2 = new HashMap<>();
ToAnalysis.parse(str2).recognition(filter).forEach(item -> {
//没有则赋初始值,有则+1
if (map2.get(item.getName()) == null){
map2.put(item.getName(),1);
}else {
map2.put(item.getName(),map2.get(item.getName())+1);
}
});
System.out.println("map1="+ JSON.toJSONString(map1));
System.out.println("map2="+ JSON.toJSONString(map2));
Set<String> set1 = map1.keySet();
Set<String> set2 = map2.keySet();
Set<String> setAll = new HashSet<>();
setAll.addAll(set1);
setAll.addAll(set2);
System.out.println("all="+JSON.toJSONString(setAll));
List<Integer> list1 = new ArrayList<>(setAll.size());
List<Integer> list2 = new ArrayList<>(setAll.size());
//构建向量
setAll.forEach(item -> {
if (set1.contains(item)){
list1.add(map1.get(item));
}else {
list1.add(0);
}
if (set2.contains(item)){
list2.add(map2.get(item));
}else {
list2.add(0);
}
});
//计算余弦相似度
int sum =0;
long sq1 = 0;
long sq2 = 0;
double result = 0;
for (int i =0;i<setAll.size();i++){
sum +=list1.get(i)*list2.get(i);
sq1 += list1.get(i)*list1.get(i);
sq2 += list2.get(i)*list2.get(i);
}
result = sum/(Math.sqrt(sq1)*Math.sqrt(sq2));
System.out.println("余弦相似度="+result);
return result;
}
SimHash+海明距离
public class SimHashAlgorithm {
private String tokens; // 字符串
private BigInteger strSimHash; //字符串产的hash值
private int hashbits = 64; //分词后的hash数
public static void main(String[] args){
//要比较的两个字符串
String[] str1 = {"abcqweqweqweqe","def"};
String[] str2 = {"ghqeqweqweqweqj","poi"};
for (int i = 0; i < str1.length; i++){
String str11 = str1[i];
String str21 = str2[i];
System.out.println(str11+"与"+str21+"相似度为:"+getPercentValue(new SimHashAlgorithm(str11, 64).getSemblance(new SimHashAlgorithm(str21, 64))));
}
}
public SimHashAlgorithm(String tokens){
this.tokens = tokens;
this.strSimHash = this.simHash();
}
private SimHashAlgorithm(String tokens,int hashbits){
this.tokens = tokens;
this.hashbits = hashbits;
this.strSimHash = this.simHash();
}
/**
* 清除html标签
* @param content
* return
*/
private String cleanResume(String content){
//若输入为HTML,下面会过滤掉所有的HTML的tag
content = Jsoup.clean(content, Whitelist.none());
content = content.toLowerCase(); //StringUtils.lowerCase(content)
String[] strings = {" ", "\n", "\r", "\t", "\\r", "\\n", "\\t", " "};
for (String s : strings) {
content = content.replaceAll(s, "");
}
return content;
}
/**
* 这个是对整个字符串进行hash计算
* @return
*/
private BigInteger simHash(){
tokens = cleanResume(tokens); // cleanResume 删除一些特殊字符
int[] v = new int[this.hashbits];
List<Term> termList = StandardTokenizer.segment(this.tokens); //对字符串进行分词
//对分词的一些特殊处理 : 比如: 根据词性添加权重 , 过滤掉标点符号 , 过滤超频词汇等;
Map<String, Integer> weightOfNature = new HashMap<String, Integer>(); // 词性的权重
weightOfNature.put("n", 2); //给名词的权重是2;
Map<String, String> stopNatures = new HashMap<String, String>();//停用的词性 如一些标点符号之类的;
stopNatures.put("w", ""); //
int overCount = 5; //设定超频词汇的界限 ;
Map<String, Integer> wordCount = new HashMap<String, Integer>();
for (Term term : termList){
String word = term.word; //分词字符串
String nature = term.nature.toString(); //分词属性
//过滤超频词
if (wordCount.containsKey(word)){
int count = wordCount.get(word);
if (count > overCount){
continue;
}
wordCount.put(word,count + 1);
}else {
wordCount.put(word,1);
}
//过滤停用词性
if (stopNatures.containsKey(nature)){
continue;
}
//2、将每一个分词hash为一组固定长度的数列,比如64bit的一个整数
BigInteger t = this.hash(word);
for (int i = 0; i < this.hashbits; i++){
BigInteger bitmask = new BigInteger("1").shiftLeft(i);
// 3、建立一个长度为64的整数数组(假设要生成64位的数字指纹,也可以是其它数字),
// 对每一个分词hash后的数列进行判断,如果是1000...1,那么数组的第一位和末尾一位加1,
// 中间的62位减一,也就是说,逢1加1,逢0减1.一直到把所有的分词hash数列全部判断完毕.
int weight = 1; //添加权重
if (weightOfNature.containsKey(nature)){
weight = weightOfNature.get(nature);
}
if (t.and(bitmask).signum() != 0){
//这里是计算整个文档所有特征的向量和
v[i] += weight;
}else {
v[i] -= weight;
}
}
}
BigInteger figerprint = new BigInteger("0");
for (int i = 0; i< this.hashbits; i++){
if (v[i] >= 0){
figerprint = figerprint.add(new BigInteger("1").shiftLeft(i));
}
}
return figerprint;
}
/**
* 对单个分词进行hash计算
* @param source
* @retun
*/
private BigInteger hash(String source){
if (source == null || source.length() == 0){
return new BigInteger("0");
}else {
/**
* 当source的长度过短,会导致hash算法失效,因此需要对过短的词补偿
*/
while (source.length() < 3){
source = source + source.charAt(0);
}
char[] sourceArray = source.toCharArray();
BigInteger x = BigInteger.valueOf(((long)sourceArray[0]) << 7);
BigInteger m = new BigInteger("1000003");
BigInteger mask = new BigInteger("2").pow(this.hashbits).subtract(new BigInteger("1"));
for (char item : sourceArray){
BigInteger temp = BigInteger.valueOf((long) item);
x = x.multiply(m).xor(temp).and(mask);
}
x = x.xor(new BigInteger(String.valueOf(source.length())));
if (x.equals(new BigInteger("-1"))){
x = new BigInteger("-2");
}
return x;
}
}
/**
* 计算海明距离,海明距离越小说明越相似;
* @param other
* @return
*/
private int hammingDistance(SimHashAlgorithm other){
BigInteger m = new BigInteger("1").shiftLeft(this.hashbits).subtract(new BigInteger("1"));
BigInteger x = this.strSimHash.xor(other.strSimHash).and(m);
int tot = 0;
while (x.signum() != 0){
tot += 1;
x = x.and(x.subtract(new BigInteger("1")));
}
return tot;
}
public double getSemblance(SimHashAlgorithm s2){
double i = (double) this.hammingDistance(s2);
return 1 - i/this.hashbits;
}
public static String getPercentValue(double similarity){
NumberFormat fmt = NumberFormat.getPercentInstance();
fmt.setMaximumFractionDigits(2); //最多两位百分小数,如 25.23%
return fmt.format(similarity);
}
}
实际应用,肯定需要基于线上大规模的数据积累,然后多种算法结合来做设备相似性判断。
四、总结
本文介绍了一种混合式设备指纹的实现方案,包含了客户端的信息收集,后端的相似度算法,都是比较简单的,实际使用还需结合业务场景和数据积累。
参考资料
1、https://www.jianshu.com/p/d82f0879f8fe
2、https://zhuanlan.zhihu.com/p/541211805