项目有需求,登录前有匿名举报功能,为了避免恶意举报,需标识出访问者进行控制或封禁。首先想到的是获取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的使用解释,对浏览器指纹不做深入研究。
目前的方案是结合上面第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 | 否 |