项目有需求,登录前有匿名举报功能,为了避免恶意举报,需标识出访问者进行控制或封禁。首先想到的是获取mac地址,网上资料很多,大体有这几种方案:
获取mac地址
-
通过浏览器获取
浏览器或是利用ActiveX,目前只有IE支持,谷歌和火狐不支持(谷歌和火狐好像有另外的插件可以支持,但没有成熟应用广泛的插件)
-
服务器获取,基本思路是先获取ip,根据ip调用nbtstat(响应有点慢) 或 arp命令,示例代码如下:
public class GetIpAndMac extends HttpServlet{ protected void service(HttpServletRequest request,HttpServletResponse res) throws IOException { try { String clientIp = getIpAddr( request); System.out.println("客户端ip为:"+clientIp); String clientMac = getMACAddress(clientIp); System.out.println("客户端mac为:"+clientMac); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 通过HttpServletRequest返回IP地址 * @param request HttpServletRequest * @return ip String * @throws Exception */ public String getIpAddr(HttpServletRequest request) throws Exception { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } /** * 通过IP地址获取MAC地址 * @param ip String,127.0.0.1格式 * @return mac String * @throws Exception */ public String getMACAddress(String ip) throws Exception { String line = ""; String macAddress = ""; final String MAC_ADDRESS_PREFIX = "MAC Address = "; final String MAC_ADDRESS_PREFIX1 = "MAC"; final String LOOPBACK_ADDRESS = "127.0.0.1"; //如果为127.0.0.1,则获取本地MAC地址。 if (LOOPBACK_ADDRESS.equals(ip)) { InetAddress inetAddress = InetAddress.getLocalHost(); //貌似此方法需要JDK1.6。 byte[] mac = NetworkInterface.getByInetAddress(inetAddress).getHardwareAddress(); //下面代码是把mac地址拼装成String StringBuilder sb = new StringBuilder(); for (int i = 0; i < mac.length; i++) { if (i != 0) { sb.append("-"); } //mac[i] & 0xFF 是为了把byte转化为正整数 String s = Integer.toHexString(mac[i] & 0xFF); sb.append(s.length() == 1 ? 0 + s : s); } //把字符串所有小写字母改为大写成为正规的mac地址并返回 macAddress = sb.toString().trim().toUpperCase(); return macAddress; } //获取非本地IP的MAC地址 try { //Process p = Runtime.getRuntime().exec("nbtstat -A " + ip); Process p = Runtime.getRuntime().exec("arp -a " + ip); InputStreamReader isr = new InputStreamReader(p.getInputStream(),"GBK"); BufferedReader br = new BufferedReader(isr); while ((line = br.readLine()) != null) { if (line != null) { /*int index = line.indexOf(MAC_ADDRESS_PREFIX1); if (index != -1) { //macAddress = line.substring(index + MAC_ADDRESS_PREFIX.length()).trim().toUpperCase(); macAddress = line.substring(line.indexOf("=")+1).trim().toUpperCase(); }*/ int index = line.indexOf(ip); if (index != -1) { //macAddress = line.substring(index + MAC_ADDRESS_PREFIX.length()).trim().toUpperCase(); macAddress = line.split("\\s+")[1].trim().toUpperCase(); } } } br.close(); } catch (IOException e) { e.printStackTrace(System.out); } return macAddress; } }
这个方案起初让我看到了曙光,而且在我本地测试没问题。但现实狠狠打了脸,这个方案有下面两个弊端:
-
提需求的人提醒了我,他们使用NAT接入互联网,NAT???好尴尬,一直都没注意网络方面的知识,查了一下才知道,简单来说就是局域网的电脑对外都只暴露了一个公有ip,那么这个局域网内电脑访问的所有请求得到的ip都是一个,ip都不准确了,mac地址即使获取到有什么意义
-
nbtstat命令只支持windows arp命令也只能在局域网中使用
所以这个方案,如果你的系统可以保证只在内网使用,那可以尝试,如果是互联网使用,根本不能使用
-
浏览器唯一标识
由于mac地址那条路走不通了,所以我想如果浏览器带唯一标识就好了,有了这个思路后就想着随便查查,结果查到了浏览器指纹(也有叫用户指纹),这也是业内比较热门的研究领域,真是意外之喜,如果单纯只是为了标识访问者,进行控制的话,这个就可以了。
浏览器指纹是指仅通过浏览器的各种信息,如系统字体、屏幕分辨率、浏览器插件,无需 cookie 等技术,就能近乎绝对定位一个用户,就算使用浏览器的隐私窗口模式,也无法匿名。这是一个被动的识别方式。也就是说,理论上你访问了某一个网站,那么这个网站就能识别到你,虽然不知道你是谁,但你有一个唯一的指纹,将来无论是广告投放、精准推送,还是其他一些关于隐私的事情,都非常方便。详细了解可参考以下链接,本文只是针对FingerPrint2的使用解释,对浏览器指纹不做深入研究。
- https://blog.csdn.net/qq_32447301/article/details/88381534
- https://www.zhihu.com/question/51410927
- https://www.jianshu.com/p/6c41658f9cb7
目前的方案是结合上面第3个链接的方案,cookie+FingerPrint2js实现
-
引入FingerPrint2js
<script src="https://cdn.jsdelivr.net/npm/fingerprintjs2@2.0.6/dist/fingerprint2.min.js"></script>
function getBrowerUUID(){
var murmur;
/**userAgent:浏览器升级会改变,进而影响计算结果
* plugins:用户安装插件可能性比较大
* fonts:字体基本都相似,但字体很多也会影响速率**/
var options = {excludes: {userAgent: true,plugins:true,fonts:true}}
var currentBrowserUUID = getcookie('browserUUID');
console.log('currentBrowserUUID:'+currentBrowserUUID);
if (window.requestIdleCallback) {
requestIdleCallback(function () {
Fingerprint2.get(options,function (components) {
console.log(components) // an array of components: {key: ..., value: ...}
var values = components.map(function (component) { return component.value })
murmur = Fingerprint2.x64hash128(values.join(''), 31)
if(!$chk(currentBrowserUUID)){
//如果当前浏览器cookie中没有browserUUID才设置cookie值,如果有还用原来的,减少浏览器属性更改对标识的影响概率
setCookie('browserUUID',murmur);
}
})
})
} else {
setTimeout(function () {
Fingerprint2.get(options,function (components) {
console.log(components) // an array of components: {key: ..., value: ...}
var values = components.map(function (component) { return component.value })
murmur = Fingerprint2.x64hash128(values.join(''), 31)
if(!$chk(currentBrowserUUID)){
setCookie('browserUUID',murmur);
}
})
}, 500)
}
}
其中为了提高计算准确率,避免用户更新浏览器,安装插件等因素影响计算结果,我把userAgent、plugins、fonts三项排除掉了,代码中有注释说明。计算的值压入cookie中,使用时获取就好。但FingerPrint2js并没有实现跨浏览器,而且也不能溯源,但对于控制恶意提交够用了,官方说明准确率达到99%,这个还没有实践,有待考证。
附录:指纹组件说明
名称解释:
取决于浏览器:当用户在同一设备上使用不同的浏览器时,某些组件不会更改,从而使设备可以进行指纹识别
稳定:每次刷新页面时,某些组件都会更改(“不稳定”)
组件 | 取决于浏览器 | 稳定 |
---|---|---|
userAgent(用户代理) | 是 | 是 |
language(语言) | 否(大部分时间) | 是 |
colorDepth(颜色深度) | 没有 | 是 |
deviceMemory(设备内存) | 没有 | 是 |
pixelRatio(像素比) | 没有 | 是 |
hardwareConcurrency(设备并发线程数) | 否(但IE不支持) | 是(但IE不支持) |
screenResolution(屏幕分辨率) | 没有 | 是 |
availableScreenResolution(屏幕有效分辨率) | 没有 | 是 |
timezoneOffset(时区偏移) | 没有 | 是 |
timezone(时区) | 没有 | 是 |
sessionStorage | 是 | 是 |
localStorage(本地存储) | 是 | 是 |
indexedDb(索引数据库) | 是 | 是 |
addBehavior(是否支持Behavior,IE5的属性) | 是 | 是 |
openDatabase(是否支持调用本地数据库) | 是 | 是 |
cpuClass(所在系统的CPU等级) | 没有 | 是 |
platform(客户端操作系统) | 否(大部分时间) | 是 |
doNotTrack(不跟踪) | 是 | 是 |
plugins | 是 | 是 |
canvas(帆布) | 是的,在实践中 | 是(大多数时候) |
webgl | 是的,在实践中 | 是(大多数时候) |
webglVendorAndRenderer | 否(大部分时间) | 是 |
adBlock(广告阻止) | 是 | 是(但可能取决于时间) |
hasLiedLanguages(用户是否篡改了语言) | 没有 | 是 |
hasLiedResolution(用户是否篡改了屏幕分辨率) | 没有 | 是 |
hasLiedOs(.用户是否篡改了操作系统) | 没有 | 是 |
hasLiedBrowser(用户是否篡改了浏览器) | 没有 | 是 |
touchSupport(触摸屏支持) | 没有 | 是 |
customEntropyFunction(自定义方法) | – | – |
fonts | 是的(大多数时候) | 是 |
audio | 是 | 是 |
enumerateDevices | ?-见#498 | 否 |