逃离 IE 浏览器与 ActiveX 控件

前言

因为公司产品是 B/S 架构,所以很多与硬件交互资源的功能,不可避免的要使用 ActiveX 控件。

但是因为安全性问题,现代浏览器例如 Chrome、Firefox、Edge 等都已经放弃了对 ActiveX 的支持,所以这个方案已经不再是合适的选择。

当然现阶段还是有很多解决方案,例如不使用新特性与新功能,坚定不移的使用支持 ActiveX 的浏览器版本;使用开源的一些浏览器组件例如 MiniBlink,开发自己的“浏览器”等等。

但是对于开发来说,面对前端涌现的很多新技术,因为浏览器版本太低,无法支持一些新特性而无法使用畏手畏脚,是一种很痛苦的开发体验。

解决打印问题

因为最初产品设计的问题,没有使用水晶报表、FastReport 等报表控件,所以打印现在还依托于 HTML 打印。

用过的其实都知道,浏览器本身以及市面上一些 ActiveX 打印控件,其实对打印功能支持都有限,一些复杂的功能无法实现,而且经常要被一些内容溢出、所见非所得等问题折磨。

而产品升级到用 Chrome 就更麻烦了,本身自带的打印功能,产品发布以后基本没有使用价值,和 C/S 架构的打印体验更是差了十万八千里。

因为之前发过一篇将打印迁移到使用 LODOP 打印控件,所以这里不再赘述:Web下打印体验最好的打印控件LODOP

现在已经将所有的打印例如条码、报表、报告单迁移到 LODOP 上。而且在不支持 ActiveX 的浏览器版本,可以使用其提供的 CLodop 控件,其会使用 HTTP 与 WebSocket 进行通信,控制打印,目前上线的项目实际体验比过去使用 ActiveX 也要好很多。

解决读卡问题

如果仅仅是磁条卡、二维码等,刷卡和扫码设备会直接将文本输出,可以使用输入框接收。

但如果是芯片卡,例如 IC 卡、身份证、社保卡、银行卡等,读卡器厂商会提供动态链接库给第三方开发使用。

一般如果是 C/S 架构,自然不会存在读卡问题,但是 B/S 架构,网上提供的资料一般都是让封装成 ocx 使用。

这里建议是参考 CLodop 的解决方案,开发一个小工具,使用 HTTP 监听处理读卡请求:

20191225160112

监听工具这里依然是 C# 的例子,可以托管到 WinForm、WPF、控制台应用程序,但是需要注意 Win7 以上 Windows 版本必须 以管理员身份运行。建议是托管于 Windows 服务。

首先我们基于 HttpListener 封装一个简单的 HttpServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/// <summary>
/// 使用HttpListener实现的HttpServer
/// </summary>
public class HttpServer
{
/// <summary>
/// 监听
/// </summary>
public HttpListener Listener { get; }

/// <summary>
/// 构造函数
/// </summary>
/// <param name="prefixes">定义url</param>
/// <param name="auth">指定身份验证 默认匿名</param>
public HttpServer(IEnumerable<string> prefixes, AuthenticationSchemes auth = AuthenticationSchemes.Anonymous)
{
if (!HttpListener.IsSupported)
{
Console.WriteLine("Windows XP SP2 or Server 2003 is required to use the HttpListener class.");
}
else if (prefixes == null || prefixes.Count() == 0)
{
Console.WriteLine("初始化服务监听失败,服务监听链接不能为空!");
}
else
{
try
{
Listener = new HttpListener
{
AuthenticationSchemes = auth
};
foreach (string prefix in prefixes)
{
Listener.Prefixes.Add(prefix);
}
}
catch (Exception exc)
{
Console.WriteLine($"开启监听服务出现未经处理的异常:{exc.Message}");
}
}
}

/// <summary>
/// Http监听程序响应事件委托
/// </summary>
/// <param name="ctx"></param>
public delegate void ResponseEventArges(HttpListenerContext ctx);

/// <summary>
/// Http监听程序响应事件
/// </summary>
public event ResponseEventArges ResponseEvent;

/// <summary>
/// 请求的回调
/// </summary>
private AsyncCallback _asyncCallback;

/// <summary>
/// 开启监听服务
/// </summary>
public void Start()
{
if (Listener == null)
{
Console.WriteLine("监听服务初始化失败,无法开启监听。");
}
else if (!Listener.IsListening)
{
try
{
Listener.Start();
_asyncCallback = new AsyncCallback(GetContextAsyncCallback);
Listener.BeginGetContext(_asyncCallback, null);
Console.WriteLine($"开启监听:{string.Join("、", Listener.Prefixes)}。");
}
catch (Exception exc)
{
Console.WriteLine($"开启监听服务出现未经处理的异常:{exc.Message}");
}
}
else
{
Console.WriteLine($"监听服务正在运行,无需重复开启。");
}
}

/// <summary>
/// 关闭服务监听
/// </summary>
public void Stop()
{
try
{
Listener?.Stop();
Console.WriteLine($"已关闭监听服务。");
}
catch (Exception exc)
{
Console.WriteLine($"关闭监听服务出现未经处理的异常:{exc.Message}");
}
}

/// <summary>
/// 异步检查传入的请求
/// </summary>
/// <param name="result">结果</param>
public void GetContextAsyncCallback(IAsyncResult result)
{
if (result.IsCompleted)
{
HttpListenerContext ctx = Listener.EndGetContext(result);
if (ResponseEvent != null)
{
ResponseEvent.Invoke(ctx);
}
else
{
dynamic data = new ExpandoObject();
data.success = false;
data.msg = $"未注册http请求处理事件。";
ctx.Response.StatusCode = 200;
// 允许跨域访问
ctx.Response.AppendHeader("Access-Control-Allow-Origin", "*");
ctx.Response.ContentType = "application/json";
ctx.Response.ContentEncoding = Encoding.UTF8;
JavaScriptSerializer serializer = new JavaScriptSerializer();
byte[] binaryData = Encoding.UTF8.GetBytes(serializer.Serialize(data));
ctx.Response.OutputStream.Write(binaryData, 0, binaryData.Length);
}
ctx.Response.Close();
}
Listener.BeginGetContext(_asyncCallback, null);
}

/// <summary>
/// 获取请求上下文中传递的参数,注意参数名已经默认转换为小写
/// </summary>
/// <param name="ctx">HttpListener请求上下文</param>
/// <returns>参数数据</returns>
public static Dictionary<string, string> GetData(HttpListenerContext ctx)
{
Dictionary<string, string> data = new Dictionary<string, string>();
if (ctx == null)
{
throw new ArgumentNullException(nameof(ctx));
}
if (ctx.Request.HttpMethod == "GET")
{
for (int i = 0; i < ctx.Request.QueryString.AllKeys.Length; i++)
{
data[ctx.Request.QueryString.AllKeys[i].ToLower()] = ctx.Request.QueryString[ctx.Request.QueryString.AllKeys[i]];
}
}
else
{
string rawData;
using (StreamReader reader = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding))
{
rawData = reader.ReadToEnd();
}
if (!string.IsNullOrEmpty(rawData))
{
string[] parameters = rawData.Split('&');
for (int i = 0; i < parameters.Length; i++)
{
string[] kvPair = parameters[i].Split('=');
if (kvPair.Length > 1)
{
string key = HttpUtility.UrlDecode(kvPair[0], ctx.Request.ContentEncoding);
string value = HttpUtility.UrlDecode(kvPair[1], ctx.Request.ContentEncoding);
data[key.ToLower()] = value;
}
}
}
}
return data;
}
}

然后我们可以借助这个封装的类型,简单的托管一个 HTTP 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Program
{
static void Main(string[] args)
{
string[] prefexes = new string[] { "http://*/read_card/" };
HttpServer server = new HttpServer(prefexes);
server.ResponseEvent += (ctx) =>
{
JavaScriptSerializer serializer = new JavaScriptSerializer();
bool success = false;
string msg = string.Empty;
string data = null;
Dictionary<string, string> parameters = HttpServer.GetData(ctx);
try
{
if (parameters.Count == 0)
{
msg = "入参不能为空!";
}
else if (!parameters.ContainsKey("token") || parameters["token"] != "sMO9sIU5OiNyIWDtKXSneWuW7TO7Kuev")
{
msg = "token 无效";
}
else if (!parameters.ContainsKey("key"))
{
msg = "无法获取必要的[key]信息!";
}
else
{
if (parameters["key"] == "USER_NAME")
{
success = true;
data = Environment.UserName;
}
else if (parameters["key"] == "USER_DOMAIN_NAME")
{
success = true;
data = Environment.UserDomainName;
}
else
{
msg = "未知的请求!";
}
}

Console.WriteLine($"响应数据:{msg}");
}
catch (Exception exc)
{
msg = $"错误:{exc.Message}";
}
finally
{
var result = new { success, msg, data };
ctx.Response.StatusCode = 200;
ctx.Response.AppendHeader("Access-Control-Allow-Origin", "*");
ctx.Response.ContentType = "application/json";
ctx.Response.ContentEncoding = Encoding.UTF8;
byte[] binaryData = Encoding.UTF8.GetBytes(serializer.Serialize(result));
ctx.Response.OutputStream.Write(binaryData, 0, binaryData.Length);
}
};
server.Start();
Console.WriteLine("监听程序启动,回车键终止程序运行!");
Console.ReadLine();
}
}

如果我们调试程序,Visual Studio 又没有使用管理员身份证运行,很容易出现以下问题:

20191225175729

如果直接双击打开应该也会有类似问题,需要右键该程序以管理员身份运行才可以,后面会另外写一篇文章,说明如何处理这个问题。

正常启动程序,并使用 Postman 访问效果如下:

20191225180916

这里给出的例子没有给出具体使用动态链接库读卡的例子,这样更方便测试,一般读卡器厂商会提供动态链接库各种编程语言的例子,抄过来就能使用。

同样的,更多需要使用 ActiveX 的功能,例如 CA 签名、U盾 等也可以使用这个方案实现。

参考: