出于信息安全的原因,以往浏览器是无法直接控制电脑上的设备的,所以,很多黑客为了控制其他电脑,一般就是做一段木马程序,通过各种办法引诱上网小白们通过浏览器下载安装一堆堆的垃圾软件,其中很有可能就有一段木马病毒通过这种方式植入到了电脑。中了木马病毒的设备就象一只待宰的羔羊,黑客们可以通过木马病毒控制电脑,偷取电脑内的数据、破坏电脑系统。在这种情况下,浏览器做为上网的窗口,如果它可以直接控制电脑上的设备是一件很有安全隐患的事情。
然而,随着互联网的发展,以B/S结构开发程序越来越占主流,B/S结构的软件都通过浏览器与操作人员交互数据,这又使得通过浏览器来控制本电脑的设备变得越来越有需求。
你看,这是不是一种非常矛盾的一种状态?但是需求一定是占主导地位的,所以,通过许多大神的辛勤、努力,开发了一些由浏览器控制电脑设备的工具,Web HID Api就是其中的一个,它可以通过浏览器直接向电脑上的HID设备发送指令,从而控制通过HID连接的电脑外设。
以下Javascript代码展示了通过 Web Hid Api ,浏览器直接向RFID读卡器发送指令读取IC卡卡号的应用。
本示例使用的读卡器:https://item.taobao.com/item.htm?spm=a21dvs.23580594.0.0.52de2c1bW5eU3X&ft=t&id=615391857885
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Web HID Api IC卡读卡器Demo</title>
<script language="javascript">
var time1;
let device; // 需要连接或已连接的设备
if ('hid' in navigator){
}else{
alert('您的浏览器不支持 Web Hid API,暂无法使用以下功能!');
}
navigator.hid.onconnect = function event() {
console.log("hid device connected: ", event.target);
}
navigator.hid.ondisconnect = function event() {
console.log("hid device disconnected: ", event.target);
}
function ParsedReturnData(databuff){
var datahex="";
if(databuff[0]==0x78 && databuff[1]==0x68 && databuff.length>4){
switch ( databuff[3]) {
case 0x0f:
textarea.value="读卡器已接受响声指令!";
break;
case 0x1E:
textarea.value="读取设备编号成功!";
for (i=5;i<9;i++){datahex=datahex+databuff[i].toString(16).padStart(2, '0').toUpperCase();}
serialnumber.value=datahex;
break;
case 0xF0:
switch(databuff[4]){
case 0x00:
textarea.value="读取IC卡号成功!";
for (i=5;i<9;i++){datahex=datahex+databuff[i].toString(16).padStart(2, '0').toUpperCase();}
carduid.value=datahex;
card8h10dz.value = parseInt("0x" + datahex).toString().padStart(10, '0');
LHCode = datahex.substring(6, 8) + datahex.substring(4, 6) + datahex.substring(2, 4) + datahex.substring(0, 2);
card8h10df.value = parseInt("0x" + LHCode).toString().padStart(10, '0');
break;
case 0x08:
textarea.value="寻卡失败,请重新将卡片放在读卡器感应区!";
break;
case 0x09:
textarea.value="选卡失败,此卡可能不是Mifare卡或感应区内有多张卡!";
break;
default:
textarea.value="寻卡失败,错误代码:"+databuff[4].toString();
break;
}
break;
case 0x10:
switch(databuff[4]){
case 0x00:
textarea.value="读取Ntag卡UID成功!";
for (i=5;i<12;i++){datahex=datahex+databuff[i].toString(16).padStart(2, '0').toUpperCase();}
ntaguid.value=datahex;
break;
case 0x08:
textarea.value="寻卡失败,请重新将Ntag卡放在读卡器感应区!";
break;
case 0x09:
textarea.value="选卡失败,此卡可能不是Ntag卡或感应区内有多张卡!";
break;
default:
textarea.value="寻卡失败,错误代码:"+databuff[4].toString();
break;
}
break;
case 0x21:
switch(databuff[4]){
case 0x00:
textarea.value="读取15693卡UID成功!";
for (i=7;i<15;i++){datahex=datahex+databuff[i].toString(16).padStart(2, '0').toUpperCase();}
uid15693.value=datahex;
break;
case 0x08:
textarea.value="寻卡失败,请重新将15693卡放在读卡器感应区!";
break;
case 0x09:
textarea.value="选卡失败,此卡可能不是15693卡!";
break;
default:
textarea.value="寻卡失败,错误代码:"+databuff[4].toString();
break;
}
break;
case 0x50:
switch(databuff[4]){
case 0x00:
textarea.value="读取二代证UID成功!";
for (i=5;i<13;i++){datahex=datahex+databuff[i].toString(16).padStart(2, '0').toUpperCase();}
sfzuid.value=datahex;
break;
case 0x08:
textarea.value="寻卡失败,请重新将二代证卡放在读卡器感应区!";
break;
default:
textarea.value="寻卡失败,错误代码:"+databuff[4].toString();
break;
}
break;
case 0x51:
switch(databuff[4]){
case 0x00:
textarea.value="读取iCLASS卡UID成功!";
for (i=5;i<13;i++){datahex=datahex+databuff[i].toString(16).padStart(2, '0').toUpperCase();}
iclassuid.value=datahex;
break;
case 0x08:
textarea.value="寻卡失败,请重新将iCLASS卡片放在读卡器感应区!";
break;
default:
textarea.value="寻卡失败,错误代码:"+databuff[4].toString();
break;
}
break;
default:
}
}
}
async function selectdev() {
try{
//const devices = await navigator.hid.requestDevice({ filters: [] });
//if (devices.length == 0) {
// console.log("No device selected\n\n");
// return;
//}
const devices = await navigator.hid.requestDevice({
filters: [{
vendorId: 0x0801, // 根据VID进行过滤
productId: 0x2011, // 根据PID进行过滤
//usagePage: 0x0c, // 根据usagePage进行过滤
//usage: 0x01, // 根据usage进行过滤
},],
});
device = devices[0]; // 选择列表中第一个设备
if (!device.opened) { // 检查设备是否打开
await device.open(); // 打开设备
}
device.oninputreport = (event) => { // 电脑接收到来自设备的消息回调
console.log(event); // event中包含device、reportId、data等内容
let array = new Uint8Array(event.data.buffer); // event.data.buffer就是接收到的inputreport包数据了
ParsedReturnData(array);
//let hexstr = "";
//for (const data of array) {
// hexstr += (Array(2).join(0) + data.toString(16).toUpperCase()).slice(-2) + " "; // 将字节数据转换成(XX )形式字符串
//}
//textarea.value += hexstr;
};
}
catch (e){
console.log(e);
}
}
function isUIntNum(val) {
var testval = /^\d+$/; // 非负整数
return (testval.test(val));
}
function isHex(val) {
var testval = /^(\d|[A-F]|[a-f])+$/; // 十六进制数判断
return (testval.test(val));
}
function ButtonDisable() { //删除按键的 onclick 事件,防止重复执行指令
document.getElementById("butt_beep").setAttribute("onclick", null);
document.getElementById("butt_getdevnum").setAttribute("onclick", null);
document.getElementById("butt_piccrequest").setAttribute("onclick", null);
//document.getElementById("butt_readloop").setAttribute("onclick", null);
}
async function beep() { //驱动发卡器响声令
if (!device?.opened) {
alert("请先选择已连接的读卡器!");
return;
}
textarea.value = "";
const outputData = new Uint8Array(32);
outputData[0]=0x78;
outputData[1]=0x68;
outputData[2]=0x04;
outputData[3]=0x0f;
outputData[4]=0x1e;
outputData[5]=0x00;
outputData[6]=0xe9;
outputData[7]=0x00;
await device.sendReport(0, outputData); // 发送数据,第一个参数为reportId,填0表示不使用reportId
}
async function getdevicenumber() { //读取发卡器唯一出厂序号,可以当加密狗使用
if (!device?.opened) {
alert("请先选择已连接的读卡器!");
return;
}
textarea.value = "";
serialnumber.value="";
const outputData = new Uint8Array(32);
outputData[0]=0x78;
outputData[1]=0x68;
outputData[2]=0x01;
outputData[3]=0x1e;
await device.sendReport(0, outputData); // 发送数据,第一个参数为reportId,填0表示不使用reportId
}
async function piccrequest() {
if (!device?.opened) {
alert("请先选择已连接的读卡器!");
return;
}
textarea.value = "";
carduid.value="";
card8h10dz.value="";
card8h10df.value="";
const outputData = new Uint8Array(32);
outputData[0]=0x78;
outputData[1]=0x68;
outputData[2]=0x01;
outputData[3]=0xf0;
await device.sendReport(0, outputData); // 发送数据,第一个参数为reportId,填0表示不使用reportId
}
function request_loop() {
if (!device?.opened) {
alert("请先选择已连接的读卡器!");
return;
}
if (document.getElementById('butt_readloop').value == '轮询读取M1卡UID') {
document.getElementById('butt_readloop').value = '停止轮询';
ButtonDisable();
time1 = setInterval("piccrequest()", 500); //开启间隔500毫秒寻一次卡
} else {
document.getElementById('butt_readloop').value = '轮询读取M1卡UID';
clearInterval(time1);
ButtonEnabled();
}
}
async function sfzrequest() {
if (!device?.opened) {
alert("请先选择已连接的读卡器!");
return;
}
textarea.value = "";
sfzuid.value="";
const outputData = new Uint8Array(32);
outputData[0]=0x78;
outputData[1]=0x68;
outputData[2]=0x01;
outputData[3]=0x50;
await device.sendReport(0, outputData); // 发送数据,第一个参数为reportId,填0表示不使用reportId
}
async function iclassrequest() {
if (!device?.opened) {
alert("请先选择已连接的读卡器!");
return;
}
textarea.value = "";
sfzuid.value="";
const outputData = new Uint8Array(32);
outputData[0]=0x78;
outputData[1]=0x68;
outputData[2]=0x01;
outputData[3]=0x51;
await device.sendReport(0, outputData); // 发送数据,第一个参数为reportId,填0表示不使用reportId
}
async function ntagrequest() {
if (!device?.opened) {
alert("请先选择已连接的读卡器!");
return;
}
textarea.value = "";
ntaguid.value="";
const outputData = new Uint8Array(32);
outputData[0]=0x78;
outputData[1]=0x68;
outputData[2]=0x01;
outputData[3]=0x10;
await device.sendReport(0, outputData); // 发送数据,第一个参数为reportId,填0表示不使用reportId
}
async function iso15693request(){
if (!device?.opened) {
alert("请先选择已连接的读卡器!");
return;
}
textarea.value = "";
uid15693.value="";
const outputData = new Uint8Array(32);
outputData[0]=0x78;
outputData[1]=0x68;
outputData[2]=0x0d;
outputData[3]=0x21;
outputData[4]=0x16;
outputData[15]=0xdd;
await device.sendReport(0, outputData); // 发送数据,第一个参数为reportId,填0表示不使用reportId
}
function ButtonEnabled() { //恢复各button 的onclick事件
document.getElementById("butt_beep").setAttribute("onclick", "beep()");
document.getElementById("butt_getdevnum").setAttribute("onclick", "getdevicenumber()");
document.getElementById("butt_piccrequest").setAttribute("onclick", "piccrequest()");
//document.getElementById("butt_readloop").setAttribute("onclick", "request_loop()");
}
window.onerror = function (e) {
ButtonEnabled(); //恢复按键的onclick事件
clearInterval(time1);
document.getElementById('butt_readloop').value = '轮询读取IC卡卡号';
alert("不好意思,出错了!");
return true;//屏蔽系统事件
}
</script>
<style>
th {
background-color:#F6FAFF;
color: blue;
font-family:楷体;
}
td {
background-color:#F6FAFF;
font-family:楷体;
}
</style>
</head>
<body>
<table width="866" height="346" align="center">
<tr>
<th width="124" height="45" scope="row"><input style="width:120px" name="butt_selectdev" type="submit" id="butt_selectdev" onclick="selectdev()" value="选择连接的读卡器" /></th>
<td width="716">
</td>
</tr>
<tr>
<th width="124" height="45" scope="row"><input style="width:120px" name="butt_beep" type="submit" id="butt_beep" onclick="beep()" value="驱动读卡器响声" /></th>
<td width="716"><input name="butt_getdevnum" type="submit" id="butt_getdevnum" onclick="getdevicenumber()" value="获取读卡器唯一出厂序列号" />
设备编号:
<input style="color:red;text-align:center;" name="serialnumber" type="text" id="serialnumber" size="8" maxlength="8" /></td>
</tr>
<tr>
<th height="80" scope="row">
<input style="width:120px" name="butt_piccrequest" type="submit" id="butt_piccrequest" onclick="piccrequest()" value="读取M1卡UID" />
<br /><br />
<input name="butt_readloop" type="submit" id="butt_readloop" style="width:120px" onclick="request_loop()" value="轮询读取M1卡UID" />
</th>
<td>原始16进制卡号:
<input style="color:red;text-align:center;" name="carduid" type="text" id="carduid" size="8" maxlength="8" />
,转8H10D正码:
<input style="color:red;text-align:center;" name="card8h10dz" type="text" id="card8h10dz" size="10" maxlength="10" />
,8H10D反码:
<input style="color:red;text-align:center;" name="card8h10df" type="text" id="card8h10df" size="10" maxlength="10" /></td>
</tr>
<tr>
<th width="124" height="45" scope="row"><input style="width:120px" name="butt_sfzrequest" type="submit" id="butt_sfzrequest" onclick="sfzrequest()" value="读取二代证UID" /></th>
<td >原始16进制卡号:<input style="color:red;text-align:center;" name="sfzuid" type="text" id="sfzuid" size="16" maxlength="16" />
</td>
</tr>
<tr>
<th width="124" height="45" scope="row"><input style="width:120px" name="butt_iclassrequest" type="submit" id="butt_iclassrequest" onclick="iclassrequest()" value="读取iCLASS卡UID" /></th>
<td>
原始16进制卡号:<input style="color:red;text-align:center;" name="iclassuid" type="text" id="iclassuid" size="16" maxlength="16" />
</td>
</tr>
<tr>
<th width="124" height="45" scope="row"><input style="width:120px" name="butt_ntagrequest" type="submit" id="butt_ntagrequest" onclick="ntagrequest()" value="读取Ntag卡UID" /></th>
<td>
原始16进制卡号:<input style="color:red;text-align:center;" name="ntaguid" type="text" id="ntaguid" size="16" maxlength="16" />
</td>
</tr>
<tr>
<th width="124" height="45" scope="row"><input style="width:120px" name="butt_15693request" type="submit" id="butt_15693request" onclick="iso15693request()" value="读取15693卡UID" /></th>
<td>
原始16进制卡号:<input style="color:red;text-align:center;" name="uid15693" type="text" id="uid15693" size="16" maxlength="16" />
</td>
</tr>
<tr>
<th height="120" scope="row"><p> </p>
<p>操作提示</p>
<p> </p></th>
<td><textarea style="color:blue;" name="textarea" id="textarea" cols="100" rows="8" ></textarea></td>
</tr>
</table>
</body>
</html>