传统的加密狗认证方案通常要求用户安装本地控件或客户端程序,浏览器在中间只能扮演一个转发角色,部署成本高且兼容性差。WebUSB标准让浏览器获得了直接访问USB设备的能力,配合jQuery简洁的DOM操作,我们可以完全在网页端完成加密狗的枚举、连接、指令交互和身份校验。本文围绕这一目标,从设备识别、通信流程到完整代码实现,逐步展开讲解。

WebUSB的工作原理与加密狗通信的基础条件
WebUSB并不是浏览器随意读写USB设备的万能通道,它建立在一套严格的权限模型之上。页面脚本无法在用户毫无感知的情况下访问任何USB设备,必须由用户主动触发一次交互(比如点击按钮),然后浏览器才会弹出设备选择器,允许用户授权特定设备给当前网站使用。这个设计避免了恶意网页静默读取用户硬件的安全风险。
加密狗本质上是一个USB HID设备或厂商自定义设备,内部通常包含一个安全芯片,支持 Challenge-Response 挑战应答、数字签名、密钥存储等操作。浏览器与加密狗通信时要明确两个关键参数:厂商ID(vendorId)和产品ID(productId)。这两个十六进制数值组合起来可以精确匹配某一款加密狗型号。除此之外,加密狗使用的传输模式也很重要,HID设备走中断传输(interrupt transfer),自定义设备则可能走控制传输或批量传输,需要根据厂商提供的协议文档确定。
需要注意的是,WebUSB默认无法访问HID类设备,因为浏览器将HID划给了独立的 WebHID API 管理。如果加密狗以标准HID方式实现,应改用 WebHID;如果加密狗使用厂商自定义的 bulk 端点,则 WebUSB 是正确的选择。购买加密狗前务必向厂商确认设备类型和支持的传输协议,这直接决定技术选型。
使用jQuery完成设备请求与连接管理
下面进入实战环节。首先在页面中放置一个按钮用于触发设备授权,因为 requestDevice 方法必须由用户手势触发,直接在页面加载时调用会抛出 SecurityError。我们用jQuery绑定点击事件,然后调用 navigator.usb.requestDevice 请求设备。
// 加密狗的厂商ID和产品ID,需根据实际设备修改
const DONGLE_FILTERS = [{ vendorId: 0x1234, productId: 0x5678 }];
$('#btn-connect').on('click', async function () {
try {
const device = await navigator.usb.requestDevice({ filters: DONGLE_FILTERS });
window.dongleDevice = device; // 保存设备句柄
await device.open(); // 打开设备
if (device.configuration === null) {
await device.selectConfiguration(1); // 选择配置
}
await device.claimInterface(0); // 声明接口
$('#status').text('加密狗连接成功:' + device.productName);
} catch (err) {
$('#status').text('连接失败:' + err.message);
}
});这段代码有几个细节值得注意。第一,requestDevice 的 filters 参数限定了只显示匹配的设备,用户在弹窗中只能看到目标加密狗,避免误操作。第二,open、selectConfiguration、claimInterface 三步是连接的固定流程,缺一不可,其中 claimInterface 申请接口的独占使用权,如果本地驱动已经占用了设备,这一步会失败。第三,我们把设备句柄保存在 window.dongleDevice 上,方便后续通信函数复用。
还有一种常见场景:用户之前已经授权过设备,希望页面加载时自动重连。这时应使用 getDevices 方法,它不需要用户手势,返回当前站点已授权的设备列表。我们可以封装一个自动检测函数,配合 disconnect 事件监听,让页面在加密狗拔出时给出提示:
// 页面加载时尝试自动重连已授权设备
$(function () {
navigator.usb.getDevices().then(function (devices) {
if (devices.length > 0) {
window.dongleDevice = devices[0];
return devices[0].open().then(function () {
return devices[0].selectConfiguration(1);
}).then(function () {
return devices[0].claimInterface(0);
});
}
return Promise.reject(new Error('没有已授权的加密狗'));
}).then(function () {
$('#status').text('已自动连接加密狗');
}).catch(function (err) {
$('#status').text(err.message);
});
// 监听设备拔出事件
navigator.usb.addEventListener('disconnect', function (e) {
if (window.dongleDevice && e.device === window.dongleDevice) {
$('#status').text('加密狗已拔出,请重新插入');
window.dongleDevice = null;
}
});
});实现挑战应答式身份认证的完整流程
连接建立后,核心工作是与加密狗交换数据。挑战应答(Challenge-Response)是最经典也最安全的认证模式:服务器生成一个随机数发给网页,网页将随机数写入加密狗,加密狗用内部密钥计算出一个签名结果返回,网页再把结果提交给服务器,服务器用数据库中登记的同源密钥(或公钥)验证结果。整个过程中密钥永远不离开加密狗芯片,即使传输被截获也无法伪造下一次应答。
以批量传输为例,数据的读写通过 transferIn 和 transferOut 完成。下面的代码演示了完整的认证流程,假设加密狗协议规定:发送8字节随机数到端点1,从端点2读取32字节签名结果。
// 将ArrayBuffer转成十六进制字符串,方便提交给服务器
function bufToHex(buf) {
return Array.prototype.map.call(new Uint8Array(buf), function (b) {
return ('0' + b.toString(16)).slice(-2);
}).join('');
}
// 执行挑战应答认证
async function dongleAuth(challengeHex) {
const device = window.dongleDevice;
if (!device) {
throw new Error('加密狗未连接');
}
// 挑战值转成字节数组
const bytes = new Uint8Array(challengeHex.match(/.{2}/g).map(function (h) {
return parseInt(h, 16);
}));
// 发送挑战值到输出端点
const outResult = await device.transferOut(1, bytes);
if (outResult.status !== 'ok') {
throw new Error('发送挑战值失败');
}
// 从输入端点读取签名结果
const inResult = await device.transferIn(2, 32);
if (inResult.status !== 'ok' || inResult.data.byteLength < 32) {
throw new Error('读取应答失败');
}
return bufToHex(inResult.data);
}
// 点击登录按钮触发认证
$('#btn-login').on('click', async function () {
var btn = $(this).prop('disabled', true);
try {
// 第一步:向服务器申请挑战值
const challenge = await $.getJSON('/api/auth/challenge');
// 第二步:让加密狗计算应答
const response = await dongleAuth(challenge.value);
// 第三步:提交应答完成登录
const login = await $.post('/api/auth/verify', {
challengeId: challenge.id,
response: response
}, null, 'json');
if (login.success) {
window.location.href = '/home';
} else {
$('#status').text('身份认证失败');
}
} catch (err) {
$('#status').text('认证异常:' + err.message);
} finally {
btn.prop('disabled', false);
}
});服务端逻辑同样重要。挑战值必须是一次性的,使用后立即作废,并设置较短的有效期(比如60秒),防止重放攻击。验证应答时,服务器根据挑战值关联的用户信息找到对应的密钥或公钥,用相同的算法重新计算并比对结果。建议挑战值长度不低于16字节,由密码学安全的随机数生成器产生。
在工程细节上,还要处理通信超时问题。transferIn 方法在设备无响应时会一直挂起,最好用 Promise.race 加一个超时包装,例如5秒内没有应答就提示用户检查加密狗是否插好。另外,不同的加密狗固件对指令格式有不同要求,有的需要在数据前加指令码和长度头,务必参照厂商的协议手册构造报文。
浏览器兼容性与生产环境的安全考量
WebUSB 目前在 Chrome、Edge 和 Opera 等基于 Chromium 的浏览器中得到支持,Firefox 和 Safari 出于安全考虑长期未实现该标准。因此在生产环境中必须做能力检测,为不支持的浏览器准备降级方案,例如提示用户改用支持的浏览器,或者回退到传统的本地控件方式。检测方法很简单,判断 navigator.usb 是否存在即可。
安全方面有几点必须重视。首先,WebUSB 要求 HTTPS 环境,本地开发可以用 localhost 调试,上线后必须配置有效的SSL证书,否则浏览器会直接禁用USB访问。其次,授权的设备信息会持久保存在浏览器中,如果用户在公用电脑上使用,应提供明确的断开连接操作,调用 device.close 释放接口,并在页面上清除状态。最后,前端完成的只是通道工作,所有认证决策必须由服务器做出,绝不能因为加密狗返回了正确格式就放行登录,应答验证一定要在服务端完成。
总结一下,WebUSB配合jQuery实现网页端加密狗认证,省去了本地控件的安装环节,用户体验和部署成本都有明显改善。开发时抓住三个关键点:确认设备类型与传输协议、严格遵循用户手势触发的授权流程、把安全验证放在服务端。处理好这些细节,这套方案完全可以胜任中高安全等级的网页身份认证场景。