在Web开发中,如果无法回避ActiveX控件——一种让用户在非IE浏览器调用控件的思路
如果Web系统需要调用特定ActiveX控件才能操作,像身份证阅读器、读卡器、获取电脑网卡MAC地址等,而且预计控件厂商在一段时间内无法解决浏览器兼容性问题,那我们能不能想法让用户在非IE浏览器中也能调用控件呢?
本文提出一种解决思路:开发一个“控件小助手”代理程序。该程序安装在用户电脑中,负责调用ActiveX控件,并在本地建立一个简易Web Server,而业务系统相关页面则通过Ajax(备注:实际是JSONP)调用代理接口,从而间接地与控件交互。
说明
假设业务系统未开启HTTPS,因为启用HTTPS后问题会变得复杂。
示意图
以身份证阅读器为例:
|-----------------------------------| |
| IE浏览器 | Windows系统 | 外设
| | |
| |------------| |------------| | |
| | JavaScript |---| ActiveX控件 |-------身份证阅读器驱动--------身份证阅读器
| |------------| |------------| | |
| | |
|-----------------------------------| |
ActiveX是微软专利,其他浏览器无法调用。然而,在用户电脑中安装一个代理程序(即“控件小助手”),便可通过代理程序来操作控件:
|------------------------------------| |
|------------------| | 代理程序 | Windows系统 | 外设
| Chrome浏览器 | | | |
| | | |--------------| |----------| | |
| |------------| | | | 小型 | | ActiveX | | 身份证 |
| | JavaScript |-----JSONP-----| Web Server |---| 控件 |--------阅读器-----------身份证阅读器
| |------------| | | |--------------| |----------| | 驱动 |
| | | | |
|------------------| |------------------------------------| |
原型
假设原先的读卡逻辑为:
<button type="button" id="readCardButton" onclick="readCard()">读取身份证</button>
<script type="text/javascript">
function readCard() {
if (window.cardReader.Check) {
var check = window.cardReader.Check();
if (check > 0) {
var ret = window.cardReader.ReadCard();
if (ret !== 0) {
// 读取不成功,需要重试
setTimeout(function () {
readCard()
}, 1000);
} else {
alert('读卡成功!身份证号码为' + window.cardReader.IdCard);
}
} else {
alert('无法读取身份证,请检查身份证阅读器是否连接到电脑,驱动是否正确安装!');
}
} else {
alert('无法读取身份证,请检查身份证阅读器控件是否正确安装!');
}
}
</script>
<object classid="clsid:xxxxxx" id="cardReader" width="0" height="0"></object>
小助手原型
我们采用C#快速实现原型程序,这样的话需要提前准备好Visual Studio。
进入Visual Studio,新建“Windows 窗体应用 (.Net Framework)”,命名为ControlHelper,并在NuGet管理器中安装Nancy。在窗体Form1中添加身份证阅读器控件,命名为CardReader。
Form1.cs代码如下:
using Nancy.Hosting.Self;
using System;
using System.Windows.Forms;
using System.Threading.Tasks;
namespace ControlHelper
{
public partial class Form1 : Form
{
private NancyHost server = null;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// 启动Web Server
server = new NancyHost(new Uri("http://127.0.0.1:12345"));
server.Start();
}
public CheckResult Check()
{
var ret = CardReader.Check();
var obj = new CheckResult();
if (ret > 0)
{
obj.Result = 1;
obj.Message = "身份证阅读器检测成功!";
}
else
{
obj.Result = 0;
obj.Message = "无法读取身份证,请检查身份证阅读器是否连接到电脑,驱动是否正确安装!";
}
return obj;
}
public async Task<IdCard> ReadIdCard()
{
var obj = new IdCard();
obj.result = 0;
obj.message = "无法读取身份证,请重试!";
try
{
for (var retry = 3; retry > 0; retry--)
{
var ret = CardReader.ReadCard();
if (ret == 0)
{
obj.Result = 1;
obj.Message = "读卡成功";
obj.IdCard = CardReader.IdCard;
// 此处省略其他属性
break;
}
System.Threading.Thread.Sleep(1000);
}
}
catch (Exception e)
{
}
return obj;
}
}
}
Helper.cs:
using Nancy;
using System;
namespace ControlHelper
{
public class Helper : NancyModule
{
public Helper()
{
Get("/idcard/check", param => Response.AsJson(Program.mainForm.Check()));
// 读卡不一定能立刻返回结果,使用async
Get("/idcard/read", async (context, t) => Response.AsJson(await Program.mainForm.ReadIdCard()));
}
}
[Serializable]
public class IdCard
{
public int Result { get; set; }
public string Message { get; set; }
public string IdCard { get; set; }
// ...
// 其他属性:姓名、性别、民族、出生日期、地址、签发机关、有效期、照片等
}
[Serializable]
public class CheckResult
{
public int Result { get; set; }
public string Message { get; set; }
}
}
代码中的监听端口为12345。为了避免冲突,可以修改成其他端口(最大65535,尽量用五位数)。由于监听地址为127.0.0.1(本机),与业务系统不同,会涉及跨域问题。为了避免离题讨论,本文代码以GET方式提供服务,并返回JSON格式数据,以便业务系统通过JSONP调用,不使用POST。
Web应用页面代码调整
调整读卡按钮的逻辑,先判断ActiveX控件是否可用。如果可用,沿用原先的ActiveX操作方式;如果不可用,则尝试通过$.ajax向“小助手”的小型Web Server发送请求。
function readCardNew() {
$('#readCardButton').prop('disabled', true);
// 先尝试调用ActiveX控件,调不通的话就调用辅助程序
if (window.cardreader.ReadCard) {
readCard();
$('#readCardButton').prop('disabled', false);
} else {
$.ajax({
url: 'http://localhost:12345/idcard/check',
dataType: 'jsonp',
timeout: 500,
cache: false,
success: function (data1) {
if (data1.Result === 0) {
alert(data1.Message);
$('#readCardButton').prop('disabled', false);
} else {
$.ajax({
url: 'http://localhost:12345/idcard/read',
dataType: 'jsonp',
cache: false,
success: function (data2) {
$('#readCardButton').prop('disabled', false);
if (data2.Result === 0) {
alert(data2.Message);
} else {
alert('读卡成功!身份证号为' + data2.IdCard);
}
},
error: function () {
alert('无法读取身份证,请重试!');
$('#readCardButton').prop('disabled', false);
}
});
}
},
error: function () {
alert('无法读取身份证,请检查小助手是否启动!');
$('#readCardButton').prop('disabled', false);
}
});
}
}
投入商业使用之前
以上是实验用原型,质量粗糙,还需要进行一些功能优化才能交付用户,例如:
- 兼顾不同操作系统的兼容性,以及不同版本.Net Framework的兼容性。(建议使用Visual C++重新开发)
- 增加校验,检测用户是否已经正确安装驱动和控件,并帮助用户完成安装(需注意遵守驱动和控件的许可协议,以免吃官司)。目前状态下,如控件未正确安装,程序一启动就会崩溃。
- 增加开机自启、版本更新等功能。
如果团队有Windows桌面应用开发经验,或者能够接受研发成本,可以考虑进一步将工具开发出来,满足用户需求,解决实际问题。当然,无论是从解决问题的角度来看,还是从开发成本的角度来看,让控件厂商去做浏览器兼容才是最合适的解决办法。
关于HTTPS
在决定开发之前,还需要特别留意一个问题:业务系统若进行HTTPS改造,“小助手”将完全失效,因为浏览器会拒绝HTTPS网站访问HTTP网站。这是浏览器的安全特性,JavaScript脚本无法干预此行为。
这样的话,需要为“localhost”签发(自签名)证书,让“小助手”也使用HTTPS服务,并要求用户(或通过安装程序自动地)将该证书添加到系统信任中。注意,除了操作系统证书库,Firefox也有自己的证书库,因此Firefox浏览器要另外添加信任。
进一步思考
从上面代码可以看出,控件读取的身份证号是明文,用户可通过浏览器控制台等途径篡改数据,带来安全隐患,而且这个漏洞无法在前端与控件层面补救。但是,如果全面改用“小助手”,便可在“小助手”一端“动手脚”,用可逆算法加密身份证信息,传到服务器之后才进行解密,从而防止用户在浏览器非法录入信息或篡改数据。