一、前言
在B/S结构的系统中,出于安全性考虑一般不准许浏览器访问客户端的硬件资源,如控制打印机,照相机等。对于一个完善系统来说,往往很多时候又需要控制这些资源。通过在浏览器中插入ActiveX插件是一种很好的解决方式。
在实际的项目开发中,遇到系统登录需要增加物理身份识别。即在系统登录的时候,除了要验证用户名和密码外,还需要验证硬件USB KEY上的信息。具体业务流程为:客户端程序读取用户硬件USB KEY里的个人信息(即加密认证信息),提交给认证 服务器 进行认证,认证 服务 器通过身份识别后,业务系统通过解析返回的XML信息判断用户是否合法有效,建立起用户和业务系统的信任通道。读取硬件USB KEY的信息我们通过本例的ActiveX控件来完成。硬件USB KEY选用飞天诚信的ePass1000ND产品。
二、概念
1、ActiveX控件
ActiveX是Microsoft提出的一组使用COM(Component Object Model,组件对象模型)使得软件组件在网络环境中进行交互的技术集。它与具体的编程语言无关。作为针对Internet应用开发的技术,ActiveX被广泛应用于WEB服务器以及客户端的各个方面。
ActiveX是从Microsoft的复合文档技术—OLE成长起来的。其基本的出发点是想让某个软件通过一个通用的机构为另一个软件提供服务,可以将其插入到WEB网页或其它应用程序中。在Internet上的使用,ActiveX特点是:一般软件需要用户单独 下载 然后执行安装,而ActiveX插件是当用户浏览到特定的网页时,IE浏览器即可自动下载并提示用户安装。 但安装的一个前提是必须经过用户的同意及确认。
2、COM技术
COM是Microsoft组件对象模型的简称。是一个说明如何建立可动态交替更新组件的规范。它提供了客户和组件为保证能够互操作应该遵循的标准。该标准对于组件架构的重要性同其他任何一个具有可交替更新部分的系统是一样的。
COM标准包括规范和实现两大部分,规范部分定义了组件和组件之间通信的机制,这些规范不依赖于任何特定的语言和 操作系统 ,只要按照该规范,任何语言都可以使用;COM标准的实现部分是COM库,COM库为COM规范的具体实现提供了一些核心服务。
在COM模型中,对象本身对于客户来说是不可见的,客户请求服务时,只能通过接口进行。一般接口是不会改变的。
3、ATL技术
ATL(Active Template Library)是微软的活动模板库,是一个产生C++/COM 代码 的框架,专门用于开发COM组件。ATL提供了小巧、高效、灵活的类,这些类为创建可互操作的COM组件提供了基本的设施。ATL完全面向COM组件,其结构完全针对COM中的诸多规范。是编写COM组件的快捷工具。
三、实现
1、项目
打开Visual Studio.Net 2005,建立一个 解决方案 或项目DeanUSBKey。在项目类型中选择Visual C++下的ATL选项,在模板中选择”ATL Project”,项目名为DeanUSBKey。点击确定,系统就在指定的目录下建立了DeanUSBKey项目和解决方案。
点击确定后,会出现建立ATL项目向导对话框,引导用户快捷方便的建立ATL项目。点击下一步,进入项目属性设置对话框,如图1所示。可以通过该对话框选择是否属性化和发布方式等。
图1 项目属性设置对话框
具体选项说明如下:
Attributed 即属性化,支持属性化编程,是未来的发展方向,是IDL方案的一种替代方案。
Dynamic-link library(DLL) 即动态链接库,表示建立一个 DLL 的组件程序。
Executable(EXE) 即可执行文件,表示建立一个 EXE 的组件程序。
Service(EXE) 即服务,表示建立一个系统服务组件程序,系统启动后就会加载并执行的程序。
Allow merging of proxy/stub code 即允许合并代理/存根代码,选择该项表示把“代理/存根”代码合并到组件程序中,否则需要单独编译,单独注册代理存根程序。
Support MFC 即支持 MFC,建议不要选择,除非有特殊的原因,比如我们原来的程序是基于MFC的,我们的组件必须要MFC的支持。一般在写 ATL 程序,不选择该项。但是很多VC程序员对于MFC的数据集合类和字符串类依赖很大,建议采用STL中的相关类进行替代。具体替换方案:
1、std::string代替MFC中的CString;
2、std::vector代替MFC的数组类如CArray,CPtrArray等;
3、std::list替换MFC中的CList等列表类;
4、对于BSTR建议采用CComBSTR 类,或_bstr_t类,本例子中就会用到该类;
Support COM+1.0 支持事务处理的 COM+ 功能。
我们选择如图1所示的选项,点击完成。ATL Project项目就生成好了,系统会在指定目录下生成一系列文件,ReadMe.txt里有各文件的文件说明。尤其要注意接口定义语言文件(DeanUSBKey.idl),它描述了对象的接口细节。
2、组件
在COM模型中,客户请求服务时,是通过接口和组件进行交互的。现在还是一个空的ATL项目,还没有任何组件。
添加组件,也就是添加ATL对象类。在DeanUSBKey项目上点击右键,添加类,弹出对话框。在类别中选择ATL。在模板中选择“ATL Simple Object”即ATL简单对象。点击确定,出现建立组件向导。如图2所示,在Short Name输入组件名称USBKey,其它内容系统会自动填写。注意组件名称不能和项目名称重名。点击下一步进入组件选项设置界面。如图3所示
图2 ATL简单对象组件名字对话框
图3 ATL简单对象组件选项对话框
具体选项说明如下:
Threading model 即线程模型,COM中的线程,这是一个复杂的部分。我们选"单元"(Apartment),它代表当在线程中调用组件函数的时候,这些调用会排队进行。如果想了解详细细节可以参看《COM技术内幕》一书。
Interface 即接口,双重(Dual),双重接口表示在一个接口中,同时支持自定义接口和 IDispatch 接口。这个非常重要,为了能够使组件能够在脚本中使用,必须选择双重接口选项。因为脚本语言的解释器只认识 IDispatch 接口。自定义接口(Custom),直接实现的是IUnknown接口。
Aggregation 即聚合,写的组件,将来是否允许被其他人以聚合方式(有聚合和包容两种方式)使用。Only(只能创建为聚合),有点类似 C++或Java 中的不能直接创建实例的虚类,如果不是处于设计目的,一般这个选项不用。大多数情况下支持“聚合”,所以我们选择“Yes”。
ISupportErrorInfo 是否支持丰富信息的错误处理接口。
Connection points 即连接点,是否支持连接点接口(事件、回调)。
IObjectWithSite 是否支持IE的调用。
我们选择如图3所示的选项,点击完成。USBKey的组件建立完成。在生成的USBKey.cpp里将是接口IUSBKey的实现。
3、接口方法
在类视图中,IUSBKey接口上点击鼠标右键。在添加项里有添加方法和属性,选择添加方法。打开图4所示的添加接口方法对话框。添加接口方法GetContent,并添加接口方法的参数。[in]表示参数方向是输入;[out]表示参数方向是输出;[out,retval]表示参数方向是输出,同时可以作为函数运算结果的返回值。一个函数中,可以有多个[in]、[out],但[retval]只能有一个,并且要和[out]组合后在最后一个位置。详细的定义说明可以参考IDL的语法说明。
图4 添加接口方法对话框
在USBKey.cpp文件里添加函数GetContent的具体实现过程。核心代码如下:
STDMETHODIMP CUSBKey::GetContent(LONG lFlags, BSTR* pUSBContent)
{
EPAS_STATUS retval;//状态
EPAS_HANDLE epsHandle ; //EPAS句柄
// 创建设备句柄
retval = epas_CreateContext(&epsHandle,0,EPAS_API_VERSION);//调用EPAS的API函数访问USB Key硬件
if (FT_SUCCESS != retval)
{return ReturnError(retval);}//返回相应的错误
// 打开设备
retval = epas_OpenDevice(epsHandle,lFlags,(void *)szAppID);
if (FT_SUCCESS != retval)
{return ReturnError(retval);}
//得到序列号
unsigned long sn[2] = {0};
retval = epas_GetProperty(epsHandle,EPAS_PROP_SERNUM,NULL,sn,sizeof(sn));
if (FT_SUCCESS != retval)
{return ReturnError(retval);}
char m_sn [8*1024+17]={0};
sprintf_s(m_sn, "%08X%08X", sn[1], sn[0]);//以16进制打印到字符串m_sn中
//得到加密字符串
//1、登录
char s[80] = "1234";//登录密码
retval = epas_Verify(epsHandle,EPAS_VERIFY_USER_PIN,(unsigned char*)s,4);
if (FT_SUCCESS != retval){return ReturnError(retval);}
//2、打开文件
EPAS_FILEINFO epsFileInfo = {0};
unsigned long epsFileID = 0x1234;//文件编号
retval = epas_OpenFile(epsHandle,0,epsFileID,&epsFileInfo,sizeof(epsFileInfo));
if (FT_SUCCESS != retval){return ReturnError(retval); }
//3、读取文件内容
unsigned long rLen = 0;
unsigned char rBuff[8*1024] = {0};
ZeroMemory(rBuff,8*1024);
retval = epas_Read(epsHandle,0,0,rBuff,epsFileInfo.ulFileSize,&rLen);
if (FT_SUCCESS != retval){return ReturnError(retval);}
//4、关闭文件
retval = epas_CloseFile(epsHandle);
//关闭设备,删除Context
retval = epas_CloseDevice(epsHandle);
retval = epas_DeleteContext(epsHandle);
strcat_s(m_sn,(char*)rBuff);
*pUSBContent=_com_util::ConvertStringToBSTR((char *)r_sn);
return S_OK;
}
为了能在函数中使用USBKey厂家提供的访问函数和使用BSTR类,需在stdafx.h头文件里面引入相应的.h和.lib文件。如下:
#include "FT_ND_API.h"// ePass1000ND的接口头文件
#include "comutil.h"
#pragma comment(lib, " FT_ND_API.lib")
#pragma comment(lib, "comsuppw.lib")
如果编译通过,VS.Net IDE会打开窗口选择执行控件的外部文件。选择regsvr32。编译成功后,组件会自动注册。可以在系统组件服务里面查看刚注册的组件DeanUSBKey。
4、错误处理
COM方法通过返回HRESULT来报告错误,其他信息异常可以通过 IErrorInfo 接口提供给客户端,这里主要讲述HRESULT返回COM方法错误。
HRESULT由一个 32 位代码组成。分为四部分,如下:
Field | Severity | Reserved | Facility | Code |
Bit(s) | 31 | 29-30 | 16-28 | 0-15 |
各字段说明:
Severity 字段是其中最重要的一个。当一个方法返回时若该字段被设置了值,就说明发生了一个错误。该字段使所有的 COM 错误代码显示为负的十进制整数。
Reserved 字段目前是预留字段。
Facility 字段为错误类别代码,总共表示 8192 种错误,由一个集中的机构负责分配这些种类。
Code 字段提供了一个可容纳 65536 个代码的空间。具体的错误代码就在该字段里面体现。
在读取USBKey信息时,捕获的错误做处理,以COM错误的形式抛出。即把前16位改为0x80FF,代码如下:
LONG CUSBKey::ReturnError(LONG retval)
{ return 0x80FF0000+retval;}
5、实现IObjectSafety接口
ActiveX控件的编写到此就可以结束了,但我们在浏览器使用改控件的过程中,经常都会弹出现在运行的脚本不安全的提示。如果给客户使用,将会带来极大不便。怎么解决呢,可以通过实现IObjectSafety接口来解决。ATL 在类 IObjectSafetyImpl 中提供了此接口的实现。如果浏览器发现你的控件支持 IObjectSafety,就在导入控件之前调用 IObjectSafety::SetInterfaceSafetyOptions 方法来确保安全性脚本操作。就不会弹出提示对话框。
在USBKey.h文件里继承类列表的末尾(class ATL_NO_VTABLE CUSBKey)加入如下语句: public IObjectSafetyImpl<CUSBKey, INTERFACESAFE_FOR_UNTRUSTED_CALLER| INTERFACESAFE_FOR_UNTRUSTED_DATA>,
并在COM 映射里添加一下行(黑体部分):
BEGIN_COM_MAP(CUSBKey)
COM_INTERFACE_ENTRY(IObjectSafety)
END_COM_MAP()
四、测试
在网页里通过脚本语言调用ActiveX控件DeanUSBKey。可以通过VBScript和JAVAScript来调用。为了能在脚本语言里使用控件接口,需知道接口的classid,可以查看接口的注册表脚本文件USBKey.rgs找到classid值。调用代码为:
<object classid="clsid: 4F 3320E4-4B66 -4C 85-8538-6E17699AAB46" id="Dean" name = "Dean" ></object>
<form id="form1" name="form1" method="post" action="">
<a href="#" οnclick="return CallUSB();">js调用ActiveX测试</a>
<input id="Write" name="Write" type="button" value="vb调用ActiveX测试" />
</form>
<SCRIPT LANGUAGE="JavaScript">
<!—
//JavaScript调用Demo
function CallUSB()
{
try {
var USBContent = Dean.GetContent(0);
alert(USBContent);
} catch (e) {
alert("错误号: " + e.number );
}
return false;
}
//-->
</SCRIPT>
<script language="VBScript" type="text/vbscript">
‘VBScript调用Demo
Sub Write_OnClick
On Error Resume Next
USBContent = Dean.GetContent(0)
MsgBox USBContent
MsgBox (err.number and &hff)
End Sub
</script>
五、部署
Internet软件分发单位是“软件包”,它由包含.INF文件或软件分发.OSD文件(或两者都包括)的.CAB文件所组成。一个分发单位也可以包含软件组件,如ActiveX控件,DLL文件等。
1、Inf文件编写
INF文件是一个文本文件,指定运行控件所需要下载或者呈交的文件(比如.DLL或者其它.OCX)。一个.INF文件就捆绑了.CAB压缩文件所有的必须文件。 缺省情况下,与现有硬盘中文件版本号相同的文件不被下载。INF文件如下:
[version]
signature="$ CHICAGO $"
AdvancedINF=2.0
[Add.Code]
FT_ND_API.dll=FT_ND_API.dll
DeanUSBKey.dll=DeanUSBKey.dll
[FT_ND_API.dll]
file-win32-x86=thiscab
DestDir=11
FileVersion=1,0,6,413
[DeanUSBKey.dll]
file-win32-x86=thiscab
RegisterServer=yes
clsid={ 4F 3320E4-4B66 -4C 85-8538-6E17699AAB46}
DestDir=11
FileVersion=1,0,0,1
[RegisterFiles]
%11%/DeanUSBKey.dll
说明:
"thiscab" 是一个关键字,指包含该INF的CAB文件。也可以从网上下载所需要的DLL文件,只要指定一个HTTP 网址即可,如:
file-win32-x86=http://www.chengdujob.net/activex/DeanUSBKey.DLL
关键字"file-win32-x86"指定平台是x86。
"FileVersion"文件版本号。
"DestDir"指的是装载目录或者文件的地址: 11指系统目录 WINDOWS/SYSTEM32;10 指Windows 目录。
2、Cab打包
Windows在系统目录中自带了CAB制作工具IExpress(/WINDOWS/system32/目录下)。打开IExpress:
1)选择“Create new Self Extraction Directive file”,点击下一步。
2)选择“Create compressed files only(ActiveX Installs)”,点击下一步。
3)点击Add,把文件添加(ft_nd_api.dll,DeanUSBKey.dll, duk_usbkey.inf)添加进去,点击下一步。
4)点击Browse,输入.CAB文件的存放地址(包含所取文件名),这里取TestCAB.CAB,并且要选中 “Store files using Long File Name inside Package”。点击下一步。
5)选择“Don’t save”,一直点击下一步,直到完成。
3、自动安装
用浏览器调用ActiveX组件或者发布组件打包文件都需要用OBJECT元素。发布.CAB文件,需要在OBJECT元素的CODEBASE特性引用包含.INF文件的.CAB文件。当访问该页面时,Internet Explorer将自动把.CAB文件作为软件分发单位下载并安装,每次访问时还会自动检测版本并进行更新。注意,浏览器出于安全性考虑,会拦截未经数字认证的控件。修改浏览器设置,在Internet选项-〉安全-〉受信任的站点-〉站点 中添加服务器地址,不要选复选框“对该区域中的所有站点要求服务器验证”。解决浏览器拦截问题,而不用更改浏览器的安全级别。
六、结束语
程序在Windows Server 2003+Microsoft Visual Studio.NET2005(C++)英文版环境下调试通过。组件技术得到越来越广泛的应用,而VC提供的活动模板库为我们创建功能强大的COM组件提供了很好的框架。通过ActiveX网页控件使网页也可以访问客户端的硬件资源。丰富B/S结构系统的功能。