轉自:http://blog.csdn.net/SHIJIERUCIMEIHAO/article/details/8827940
網上看到些文章講述關於C++創建ActiveX插件的講述,覺得比較好,特保存起來
一般的Web應用對於瀏覽器插件能不使用的建議儘量不使用,因爲其涉及到安全問題以及影響用戶安裝(或自動下載註冊安裝)體驗問題。在有特殊需求(如涉及數據安全的金融業務數據交互、需插件才能實現的與本地設備的交互等)的情況下可以酌情慎用。
瀏覽器插件總體可以劃分爲兩大陣營,即IE支持的插件以及非IE支持的插件。本來在Netscape時代,對於瀏覽器插件是有公用的規範的(NPAPI),一開始所有瀏覽器都支持該規範,包括IE。後來出於商業原因,微軟的IE不再支持NPAPI,改而自己開發了一套基於COM的ActiveX體系,但這個體系對於非IE瀏覽器是拒絕支持的。所以目前的狀況基本是,IE瀏覽器僅支持ActiveX控件,而Firefox、Chrome等瀏覽器只支持另一類接口(XPCOM或NPAPI)。要想實現一個Web插件,至少需要同時考慮IE支持的AceiveX版以及非IE支持的Plugin版(Flash等插件對於IE與非IE瀏覽器都是不同的)。
ActiveX的開發可以用C#、VB及C++等語言。用C++開發ActiveX既可以使用ATL,也可以使用MFC。ATL ActiveX輸出文件較小,適合網絡傳輸,但開發複雜度稍大;而MFC ActiveX輸出文件稍大(附帶必要的MFC dll),但易於上手。本文主要介紹基於MFC的ActiveX開發。
一、創建項目及添加接口
在Vs.net 2008中,新建一個MFC ActiveX Control項目:
點擊“OK”後將彈出如下對話框:
依次點擊“Next”按鈕直到“Control Settings”標籤頁:
由於本例子只演示僅提供函數接口不基於界面的ActiveX,故“Create control based on”選擇“(none)”即可。點擊"Finish”按鈕,即完成了項目的創建,文件結構如下:
右擊項目名稱,選擇“Properties”,在項目屬性對話框中對“All Configurations”進行配置。在“Configurations Properties->General”標籤頁中,“Use of MFC”選擇“Use MFC in a static Library”,以便編譯時將MFC相關庫自動和控件一起打包。對於“Character Set”的選擇根據具體情況而定,須注意“Unicode Character Set”和“Mulity-Byte Character SEt”對字符處理是完全不一樣的(字符編碼不一樣,需要進行MultiByteToWideChar或WideCharToMultiByte轉換)。
注意:創建MFC ActiveX Control時已經自動給項目添加了.def文件並做好了相應關聯。若對配置信息更改後導致編譯的ocx註冊不成功或提示找不到EntryPoint,可以檢查一下Linker->Input的Module Definition File是否配置正確,正常情況下已經自動配置好了,如下圖:
接下來就可以在ActiveX中添加我們需要與外部交互的接口方法和屬性了。選擇“Class View”,右擊“MyTestActiveXLib->_DMyTestActiveX”,在彈出的菜單中可以選擇Add Function或Add Property來添加接口方法或接口屬性:
這裏以定義一個LONG AddFun(LONG num1,LONG num2) 的接口函數爲例,添加Menthod如下圖所示:
點擊Finish後,即可在“MyTestActiveXCtrl.cpp”文件找到剛添加的接口函數代碼:
在函數體中完成自定義的業務邏輯即可。
二、實現安全接口
上述項目編譯後即可生成ocx文件,該ocx即可嵌入html在IE中運行。但如果該ocx對應頁面是放在真實的web服務器上,訪問該頁面執行ActiveX裏對應接口時IE將會提示“無相關屬性,需要設置其初始化和腳本運行的安全性”等信息。這是因爲ActiveX要在遠程IE上執行,需要實現安全接口。有關控件的初始化和腳本安全問題,《再談IObjectSafety》一文及其引用的Microsoft文章做了較詳致描述。
對於ATL寫的ActiveX,實現IObjectSafety即可,這裏有ATL實現安全接口的詳細的描述。
對於MFC寫的ActiveX,可以通過修改註冊表的方式來實現控件的安全性,微軟也提供的詳細的文檔描述。具體實現步驟如下:
1、首先在項目中添加Cathelp.h和Cathelp.cpp兩個文件,其內容如下所示。
Cathelp.h
#include "comcat.h" // Helper function to create a component category and associated // description HRESULT CreateComponentCategory(CATID catid, WCHAR* catDescription); // Helper function to register a CLSID as belonging to a component // category HRESULT RegisterCLSIDInCategory(REFCLSID clsid, CATID catid); // HRESULT UnRegisterCLSIDInCategory - Remove entries from the registry HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid, CATID catid);
Cathelp.cpp
#include "stdafx.h" #include "comcat.h" #include "strsafe.h" #include "objsafe.h" // HRESULT CreateComponentCategory - Used to register ActiveX control as safe HRESULT CreateComponentCategory(CATID catid, WCHAR *catDescription) { ICatRegister *pcr = NULL ; HRESULT hr = S_OK ; hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr); if (FAILED(hr)) return hr; // Make sure the HKCR\Component Categories\{..catid...} // key is registered. CATEGORYINFO catinfo; catinfo.catid = catid; catinfo.lcid = 0x0409 ; // english size_t len; // Make sure the provided description is not too long. // Only copy the first 127 characters if it is. // The second parameter of StringCchLength is the maximum // number of characters that may be read into catDescription. // There must be room for a NULL-terminator. The third parameter // contains the number of characters excluding the NULL-terminator. hr = StringCchLength(catDescription, STRSAFE_MAX_CCH, &len); if (SUCCEEDED(hr)) { if (len>127) { len = 127; } } else { // TODO: Write an error handler; } // The second parameter of StringCchCopy is 128 because you need // room for a NULL-terminator. hr = StringCchCopy(catinfo.szDescription, len + 1, catDescription); // Make sure the description is null terminated. catinfo.szDescription[len + 1] = '\0'; hr = pcr->RegisterCategories(1, &catinfo); pcr->Release(); return hr; } // HRESULT RegisterCLSIDInCategory - // Register your component categories information HRESULT RegisterCLSIDInCategory(REFCLSID clsid, CATID catid) { // Register your component categories information. ICatRegister *pcr = NULL ; HRESULT hr = S_OK ; hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr); if (SUCCEEDED(hr)) { // Register this category as being "implemented" by the class. CATID rgcatid[1] ; rgcatid[0] = catid; hr = pcr->RegisterClassImplCategories(clsid, 1, rgcatid); } if (pcr != NULL) pcr->Release(); return hr; } // HRESULT UnRegisterCLSIDInCategory - Remove entries from the registry HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid, CATID catid) { ICatRegister *pcr = NULL ; HRESULT hr = S_OK ; hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr); if (SUCCEEDED(hr)) { // Unregister this category as being "implemented" by the class. CATID rgcatid[1] ; rgcatid[0] = catid; hr = pcr->UnRegisterClassImplCategories(clsid, 1, rgcatid); } if (pcr != NULL) pcr->Release(); return hr; }
注:Cathelp.cpp中的代碼是基於Unicode Character Set的。故項目配置時若改成Multi-Byte Character Set,需對Cathelp.cpp中代碼做相應修改,否則編譯不過;
2、在MyTestActiveX.cpp文件中,添加CLSID_SafeItem的定義:
CLSID_SafeItem的值是根據xxxCtrl.cpp(本例中是MyTestActiveXCtrl.cpp)文件中IMPLEMENT_OLECREATE_EX的定義而來的(實際上就是ActiveX的CLASSID)。本例中MyTestActiveXCtrl.cpp文件中IMPLEMENT_OLECREATE_EX的的值如下:
將“0x1345c26b, 0xe979, 0x45a5, 0x99, 0x7d, 0x94, 0x27, 0xfb, 0x81, 0xe7, 0x7”簡單的在適當位置添加“{”和“}”括弧即變成了CLSID_SafeItem的值“0x1345c26b, 0xe979, 0x45a5, {0x99, 0x7d, 0x94, 0x27, 0xfb, 0x81, 0xe7, 0x7}”。
另外,MyTestActiveX.cpp文件起始處還需要引入如下兩個文件方能正常編譯:
3、修改MyTestActiveX.cpp中DllRegisterServer和DllUnregisterServer函數,代碼如下(照抄即可):
// DllRegisterServer - Adds entries to the system registry STDAPI DllRegisterServer(void) { HRESULT hr; // HResult used by Safety Functions AFX_MANAGE_STATE(_afxModuleAddrThis); if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid)) return ResultFromScode(SELFREG_E_TYPELIB); if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE)) return ResultFromScode(SELFREG_E_CLASS); // Mark the control as safe for initializing. hr = CreateComponentCategory(CATID_SafeForInitializing, L"Controls safely initializable from persistent data!"); if (FAILED(hr)) return hr; hr = RegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForInitializing); if (FAILED(hr)) return hr; // Mark the control as safe for scripting. hr = CreateComponentCategory(CATID_SafeForScripting, L"Controls safely scriptable!"); if (FAILED(hr)) return hr; hr = RegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForScripting); if (FAILED(hr)) return hr; return NOERROR; } // DllUnregisterServer - Removes entries from the system registry STDAPI DllUnregisterServer(void) { AFX_MANAGE_STATE(_afxModuleAddrThis); // 刪除控件初始化安全入口. HRESULT hr=UnRegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForInitializing); if (FAILED(hr)) return hr; // 刪除控件腳本安全入口 hr=UnRegisterCLSIDInCategory(CLSID_SafeItem, CATID_SafeForScripting); if (FAILED(hr)) return hr; if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor)) return ResultFromScode(SELFREG_E_TYPELIB); if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE)) return ResultFromScode(SELFREG_E_CLASS); return NOERROR; }
注: 很多例子裏DllUnregisterServer的寫法與本文的寫法不一致,結果導致卸載控件時(regsvr32 /u xxxx.ocx)出現“調用某某ocx文件的DllUnregisterServer函數出錯,錯誤代碼:0x80070002”錯誤。究其根源,是DllUnregisterServer中刪除註冊表的順序出了問題,“waxgourd0的專欄”中有篇文章對此做了詳盡描述。
4、在解決方案下點擊資源文件(Resources->MyTestActiveX.rc),點擊右鍵在彈出的菜單中選擇“View Code”, 編輯資源文件信息並確保以下幾個項目的正確性:
a) BLOCK的值爲“040904e4”
b) OLESelfRegister的值爲“\0”
c) VarFileInfo中的Translation後對應爲“0x0409, 1252”
到目前爲止,可以編譯項目,輸出的ocx控件是可以正常運行的了。~~~
按照上文的步驟,能開發一個基於MFC的簡單的ActiveX控件。不過在實際操作中還是會遇到一些問題。由於對COM編程瞭解得很少很少,有些問題我也沒有找到很好的解決方法。
一、ActiveX需要引用其他dll的問題
我們的ActiveX需要對IC卡設備進行讀寫,所以需要調用設備自帶的接口。設備廠商提供了“mwhrf_bj.lib”、“mwhrf_bj.dll”和“mwrf32.h”等接口文件。將“mwhrf_bj.lib”和“mwrf32.h”添加到項目中,ActiveX的接口方法中就可以調用接口文件中的方法了。但是在編譯時會出現“Project:error PRJ0050:未能註冊輸出。請嘗試啓用“每個用戶的重定向”,或用提升權限從命令提示窗口中註冊該組件”或“Project : error PRJ0050: Failed to register output. Please ensure you have the appropriate permissions to modify the registry”的錯誤。
實際上該錯誤不是出現在編輯階段,而是出現在註冊編譯後的ocx文件時。Vs.net 2008默認在編譯成功後會自動註冊編譯後的ocx文件。右擊項目名稱,選擇“Properties”,在彈出對話框的“Configurations Properties->Linker->General”中的Register Output就可以配置編譯後是否自動註冊ocx,如下圖所示:
之所以註冊ocx時出錯,是因爲註冊時找不到被調用的“mwhrf_bj.dll”文件。將被調用的“mwhrf_bj.dll”文件放在ocx文件相同目錄下或者其他%PATH%路徑下(如Windows文件夾或System32文件夾等),則註冊ocx時不會報錯。在vs.net開發環境中可以直接將要被調用的外部dll文件copy到Debug或Release目錄下即可,也可以在PreBuild腳本里將外部dll文件COPY到編譯目標文件夾,如:
注:可參考“http://www.cnblogs.com/lidabo/archive/2012/07/16/2593604.html”文章。
二、ActiveX的調試方法
在Vs.net 2008下可以對ActiveX按如下方式進行調試:
1、準備好Demo.html文件並寫好測試程序,該頁面中需通過<object />來引用需測試的ocx控件(關於如何在html頁面中調用控件在後續文章將專門提及)。
2、在vs.net 2008中右擊項目名稱,選擇“Properties”,在彈出框中的Debugging配置頁裏配置好Command和CommandArgs參數:
Command: 本地IE瀏覽器的路徑,如“C:\Program Files\Internet Explorer\IEXPLORE.EXE”
Command Args: 已經創建好的用於測試ocx的html文件路徑(如上面提及的Demo.html文件路徑)
3、在程序中需調試的地方設置斷點。按F5運行後vs.net將自動啓動IE並打開對應的html測試文件,在斷點處會中斷運行進入調試狀態。
三、ActiveX的接口實現out/ref參數及返回自定義結構體數據的問題
有時候ActiveX的接口方法只返回一個數據並不能滿足我們的實際要求。例如通過ActiveX的getPersons()方法返回一堆的人員信息,那必定是一個列表或數組,而且每個Person還包含姓名、性別等各種信息,這個時候返回值就相當複雜了。
爲了簡單起見,還是已通過ActiveX進行讀卡號來舉例。一般情況下,只要該插件提供以下接口即可滿足需求:
BSTR ReadCardNo();
這樣在javascript中調用該ActiveX的ReadCardNo()方法即可返回一個包含卡號的字符串。
但是,僅僅提供這個接口如何來識別讀卡過程中出現的異常呢?如果讀卡操作一切正常,返回一個卡號字符串當然沒有問題。但如果讀卡過程中出現諸如讀卡設備未正確連接、卡無法識別等情況,如果將這些異常信息反饋給調用者呢?
1、首先我想到的是使用ref或out參數來解決,對應C++裏是OUT/RETVAL之類的參數修飾符。
在.idl中定義接口爲:
[id(1), helpstring("方法ReadCardNo")] LONG GetSheetName([out]BSTR* cardNo);
對應接口原型爲:
LONG ReadCardNo( BSTR* cardNo );
這樣的話通過LONG類型的返回值來識別返回狀態,例如可以約定:
0-讀卡成功
1-讀卡設備未連接
2-未找到可識別的卡
……
如果返回值爲0,表示讀卡成功,讀出的卡號已通過out類型的參數cardNo傳遞給調用者。
但是,javascript等腳本語言並不支持out/ref等類型的參數,函數參數也無法傳址,所以這個方案無法解決我的實際問題。
2、如果ActiveX的接口能返回一個自定義的結構體類型數據就能滿足我們的需求了。例如我們定義一個結構體:
typedef struct
{
LONG ResultStatus, // 返回狀態 0-讀卡成功 1-讀卡設備未連接 3-未找到可識別的卡
BSTR CardNo // 讀卡成功時,保存讀取的卡號
} AOPResult;
對應接口如果可以按如下樣子來實現就可以解決我們的問題了:
AOPResult ReadCardNo();
但是,在MFC ACtiveX的接口定義中中不能直接使用自定義的數據類型的,需要用VARIANT類型來進行轉換。下面幾篇參考文章均對此有所描述:
a) http://bbs.csdn.net/topics/320146859
b) http://bbs.csdn.net/topics/20064135
c) http://www.codeproject.com/Articles/916/Using-User-Defined-Types-in-COM-ATL
d) 標準MFC WinSock ActiveX控件開發實例(II)高級篇
但實現起來也不是那麼容易,鑑於時間問題及我們實際需求的不迫切性,我對此沒有做過多嘗試。如果有成型實例,望請賜教。
3、既然在Web應用場景下ActiveX的接口一般都是供js調用,那麼我們可以返回一個json類型的數據即可,如“{ status:0, cardNo:234234344634 }”。這樣ActiveX接口仍然只需返回一個BSTR的參數,只是返回值的意義變了,不是簡單的卡號,而需要ActiveX的ReadCardNo接口在內部處理時需要將返回值封裝成一個json格式的字符串返回並交由調用方解析。不過,在封裝json字符串時需要對{、}、:等特殊字符進行相應處理。
4、對於簡單的應用場景,我們也完全可以利用ActiveX的屬性來化解此類問題。例如我們在ActiveX中定義一個屬性CardNo,這樣的話提供的接口只用簡單的返回一個狀態即可:
LONG ReadCardNo()
接口返回值仍然表示狀態,如0表示讀卡成功,1表示未找到讀卡設備等等。當返回0時,讀卡成功,對應的卡號從屬性CardNo中獲取即可。
ActiveX插件如果想在Html中進行引用,必須先對插件ocx文件進行註冊,即通過regsvr32將該控件註冊到用戶的操作系統裏。在實際應用中,一般有兩種方式來達到這個目的:
一、通過安裝程序註冊ActiveX
這種方式非常直觀,就是製作一個簡單的安裝程序,該安裝程序的任務就是將打包的ocx文件及其依賴文件解壓複製到系統目標位置,然後再通過執行regsvr32命令將已複製到用戶機器目標位置的ocx文件註冊到系統中。當web頁面中需要調用相應的ActiveX時,將在顯著位置提示用戶需下載指定的程序並運行安裝。
實際很多應用程序在安裝時都隱含地向系統註冊了一些ActiveX的,例如QQ、飛信、播放器等,這樣相應的web就更加靈活。不過,並不是所有的ActiveX插件都是以ocx文件呈現的,也可以是dll文件。
二、通過cab包由IE自動註冊
能否在web頁面需要引用ActiveX時由IE自動下載對應的插件並自動安裝呢?當然可以。我們要做的就是要將ocx及其他文件打包成一個cab文件,然後將該cab文件放在web服務器上,並在web頁面中通過<object ….. codebase=”xxx.cab#version=1,0,0,1” />的方式進行調用。
cab實際上是微軟規定的一個特殊格式的壓縮文件,製作cab包過程很簡單:
1、 準備cabarc.exe工具,該工具可以在這裏下載,也可以從微軟下載;
2、 將ocx文件及依賴的其他文件放到同一個目錄下,並在該目錄下創建一個後綴爲.inf的文件(文件名可任意取,一般與ocx文件同名,例如MyTestActiveX.inf),文件內容如下:
[version]
signature="$CHICAGO$"
AdvancedINF=2.0
[Add.Code]
MyTestActiveX.ocx=MyTestActiveX.ocx
mwhrf_bj.dll = mwhrf_bj.dll
[MyTestActiveX.ocx]
file=thiscab
clsid={1345C26B-E979-45A5-997D-9427FB81E707}
FileVersion=1,0,0,1
RegisterServer=yes
DestDir=11
[mwhrf_bj.dll]
file=thiscab
FileVersion=1,0,0,0
DestDir=10
a) signature="$CHICAGO$"表示這個.INF文件和Windows95或其後版本和Windows NT 4.0或其後的版本兼容。
b) [Add.Code]下面的內容用於定義該cab需要下載的各文件對應的定義區塊,左邊爲文件名,等號右邊爲定義區域名,一般爲易讀均將定義的區域名與文件名相同。
c) 各文件的定義區域分別定義了該文件的各種屬性:
file:表示該文件的獲取位置,此處thiscab表示該文件就包含在該cab中;如果在其他位置而不在cab包中,則可以寫上具體的位置如http://xxx.xxx.xxx/xx/mwhrf_bj.dll
clsid: 只有需要註冊的ocx文件才設置這個屬性,他的值就是改ocx的唯一classid,可以從項目的.idl文件中最下方查找;
FileVersion:文件版本號。一般將ocx文件的版本號視同爲整個cab的版本號,在<object codebase=”xx.cab#version=1,0,0,1”中將用到該版本號。
DestDir:該文件需要COPY到目標機器的位置,11表示system32目錄下,10表示windows目錄下,……
有關inf文件的具體內容可參考以下文章相關部分,已經非常詳細了:
VC2005從開發MFC ActiveX ocx控件到發佈到.net網站的全部過程
注:如果考慮到終端用戶的權限以及將ActiveX註冊到什麼位置(Current User或Machine),可參考
3、運行如下命令進行打包:
cabarc" -s 6144 N "xxxxxx.cab" "xxxxx.ocx" "mwhrf_bj.dll" "xxxxxx.inf"
其中凡是需要打包的文件均要一一列出,inf文件放在最後(未測試是否必須最後)。文件路徑均可以是絕對路徑或相對路徑,不一定非得是相同文件夾下。
命令執行後將自動生成.cab文件。
簡單總結一下前幾篇文章的內容,簡單介紹了一下如何在Vs.net 2008下用C++開發基於MFC的ActiveX插件,介紹了開發插件時可能遇到的問題,介紹瞭如何註冊插件以及如何打包成cab文件。但是,到目前爲止還沒有專門提及如何在Web頁面中調用插件,本文主要針對這個問題進行展開。
一、用<Object>標籤調用ActiveX
1、Object標籤基本用法
在Html頁面中調用ActiveX插件最簡單常用的方法是:
<object id="CardAccessor" classid="clsid:03AD53E8-D7E7-485D-A39A-D07B37DEFBC9" width="0" height="0"> </object>
id屬性就不用解釋了,和html中其他元素的id一樣,是DOM樹中各元素的唯一標識。width和height表示該ActiveX在Web頁面中佔位的大小,對於僅提供接口無UI界面的ActiveX來說將其設置爲0即可,因爲不需要在頁面上顯示任何內容(對於需要顯示界面的ActiveX,需要在項目裏創建Dialog及寫相應邏輯,可以參考“A Complete ActiveX Web Control Tutorial”實例 )。
classid屬性在這裏是一個非常關鍵的屬性,IE正是通過他才能正確找到要調用的ActiveX的。每個ActiveX均有一個唯一的id來表示,這就是classid,在我們創建MFC ActiveX Control項目時Vs.net 2008就幫我們生成了這個id, 可以在程序的.idl文件最下方找到這個ID值:
一般不建議手動修改程序中的這個uuid值,因爲在xxxxCtrl.cpp文件中也用到了這個id值,只是表現形式不一樣罷了:
控件註冊成功後,這個classid及控件文件位置等信息均寫入註冊表了,如下所示:
當然,如果ActiveX還定義了其他屬性,也可以在<object>中以屬性的形式給他們賦值。
如果用戶的計算機已經註冊了該插件(例如通過Setup.exe方式),那麼Html引用上段代碼後就可以通過js調用插件的接口和屬性了(再次提示一下,ActiveX只能在IE瀏覽器運行,也就是<object…>這段代碼在firefox等其他瀏覽器是不能正常工作的)。
<fieldset> <legend>Read Card No Testing</legend> 卡號:<input type="text" id="txtCardNo_Read" maxlength="32" class="txt disable" readonly /> <input type="button" id="btnRead" value=" Read CardNo " class="btn" onclick="javascript:readCardNo();" /> </fieldset> <fieldset> <legend>Write Card No Testing</legend> 卡號:<input type="text" id="txtCardNo_Write" maxlength="32" class="txt" /> <input type="button" id="btnWrite" value=" Write CardNo " class="btn" onclick="javascript:writeCardNo();" /> </fieldset> <script type="text/javascript"> var txtCardNo_Read = document.getElementById("txtCardNo_Read"); var txtCardNo_Write = document.getElementById("txtCardNo_Write"); var objCard = document.getElementById("CardAccessor"); function readCardNo() { txtCardNo_Read.value = ""; try{ var ret = objCard.ReadCardNo(); if (ret == 0) { txtCardNo_Read.value = objCard.CardNo; alert("讀卡成功!"); } else { alert("讀卡失敗!錯誤碼爲:" + ret); } } catch (e) { alert(e.message) } } function writeCardNo() { var cardNo = txtCardNo_Write.value; try { objCard.CardNo = cardNo; var ret = objCard.WriteCardNo(); if (ret == 0) { alert("寫卡成功!"); } else { alert("寫卡失敗!錯誤碼爲:" + ret); } } catch (e) { alert(e.message) } } </script>
2、使用Object標籤時如何判斷ActiveX是否已註冊
<script type="text/javascript"> var objCard = document.getElementById("CardAccessor"); if (objCard.object==null) { alert("CardAccessor插件未安裝!"); } else{ alert("已檢測到CardAccessor插件!"); } </script>
另外也可以通過訪問ActiveX中的某個已知已定義的屬性來判斷插件是否已安裝,如果返回的屬性值爲undefined,則表示沒有檢測到插件。例如:
<script type="text/javascript"> var objCard = document.getElementById("CardAccessor"); if (objCard.CardNo==undefinedl) { alert("CardAccessor插件未安裝!"); } else{ alert("已檢測到CardAccessor插件!"); } </script>
3、如何讓IE自動下載安裝插件並智能升級
如果檢測到插件沒有安裝,怎樣讓IE自動從指定位置下載插件並自動安裝呢?很簡單,在object標籤中使用codebase屬性即可:
<object id="CardAccessor" classid="clsid:03AD53E8-D7E7-485D-A39A-D07B37DEFBC9" codebase="CardAccessor.cab#version=1,0,0,1" width="0" height="0"> </object>
codebase的值格式爲“xxxxx.cab#version=1,0,0,1”。'#'前面部分爲cab文件的位置,可以是在服務器上的絕對位置,也可以是相對位置。'#‘後面部分表示當前引用的cab包的版本號。當IE檢測到系統沒有註冊指定插件,便從codebase指定的位置下載該cab到本地,並按照其中.inf文件的描述將各文件複製到指定位置並註冊指定的控件。(注:在實際應用中涉及簽名問題,後文再述)
即使本地已經註冊了該插件,IE還將拿已註冊的控件版本號與codebase中指定的版本號相比較,如果codebase中指定的版本號大於已註冊插件的版本號,IE仍然會從codebase指定位置下載cab包並重新註冊該插件。
正如前面的文章所提,爲方便管理,一般將cab包中需註冊的ocx文件的版本號視同cab版本號。
當插件或插件依賴的文件需要升級時,只需更新相應的文件,並對.inf中的相應文件版本升級(爲方便管理,無論是否更新了ocx文件,該ocx文件的版本號也跟着升級,因爲其版本號代表了整個cab),然後重新打包成cab發佈到服務器上,並更新html中object標籤中codebase屬性值的version部分版本號。當用戶下次訪問該頁面時,IE將自動下載升級後的cab並重新註冊插件。實際上,經過我的測試,及時服務器上的cab包不做任何變化,只要增加codebase中version的值,對應插件均會重新下載和註冊。
二、通過javascript的new ActiveXObject來調用ActiveX
如果不使用object標籤,也可以直接通過js的ActiveXObject來創建指定ActiveX的實例從而達到調用插件接口的目的(IE下xmlhttpRequest的調用就是這個原理)。例如:
var objCard = new ActiveXObject("Uprain.CardAccessorCtrl.1");
如果插件已經註冊,接下來就可以通過objCard來調用插件接口和訪問屬性了。
ActiveXObject函數的參數爲對應插件的ProgId而非CLASSID。在項目中xxxxCtrl.cpp文件中同樣可以找到或修改對應插件的ProgId值,如下圖:
即IMPLEMENT_OLECREATE_EX的第二參數就表示當前插件的ProgId,該值可以根據實際需要自行修改。實際上,在註冊表中通過ProgId是可以找到對應的ClassId的,兩者是有關聯的:
那麼如何判斷ActiveX是否已經安裝呢?實際上如果ActiveX未安裝,通過new ActiveXObject的方式來創建插件對象是會拋出“Automation 服務器不能創建對象”異常的。所以用如下方式即可:
try { objCard = new ActiveXObject("Uprain.CardAccessorCtrl.1"); } catch (e) { alert("調用ActiveX失敗!"); }
不過,我在實際測試過程中遇到兩種情況:
1) 出現“Automation 服務器不能創建對象”異常的並不一定就表示插件沒安裝,也有可能是因插件未實現初始化或腳本安全接口,從而被IE攔截,需要調整IE“工具-選項-安全-自定義級別”中“ActiveX控件和插件”部分的設置;
2) 有時候,同樣已註冊的插件,通過object標籤引用的方式能正常調用接口,但通過new ActiveXObject的方式則調用插件接口失敗。
前面四篇文章都是在描述如何用C++開發基於MFC的ActiveX插件以及如果對插件進行打包和在Web頁面中調用,但確忽略了一個非常重要的問題:代碼簽名。《瀏覽器插件之ActiveX開發(三)》提及了兩種註冊插件的方法,其中IE自動下載並註冊插件的方法就涉及到簽名問題,如果cab包是未簽名的或簽名不被信任的,IE就拒絕註冊該插件。
一、數字簽名簡述
現在的各種軟件星羅密佈、魚龍混雜,用戶在使用軟件程序時一定要十分謹慎,稍不留意,就被病毒或惡意程序侵害。代碼數字簽名的出現就在一定程度上解決了這個問題。那麼經過數字簽名的軟件有什麼好處?軟件一旦經過數字簽名,至少可以保證以下兩點:
1)該軟件確實是由數字簽名證書中顯示的軟件開發商開發的;
2)該軟件自軟件開發商發佈以後,沒有被第三方做過任何修改。
不過,要完全理解數字簽名或PKI(數字簽名是PKI的組成部分,Public Key Instructure),還需要理解一下基本常識,例如對稱加密、非對稱加密、摘要算法、公鑰、私鑰、數字證書、根證書等等。以下幾篇文章均對這些概念做了通俗易懂的解讀:
a) 白話數字簽名
b) CA認證原理以及實現
c) 瞭解數字證書
d) 軟件代碼數字簽名基本原理
e) 數字簽名
f) 爲什麼需要PKI
g) 數字簽名(代碼簽名)流程
二、爲什麼ActiveX需要數字簽名?
由於ActiveX插件在運行時與本地桌面應用程序一樣,對用戶系統的資源有極大的訪問權限,如果讓任何ActiveX通過Web頁面都能自動下載並註冊的話,那對用戶必將造成非常大的威脅。所以默認設置下,IE將對需自動下載的cab文件進行數字簽名認證,只有經過數字簽名了且簽名認證通過的cab包才自動下載並註冊到用戶系統中。
一般地,不僅僅需要對cab包進行代碼簽名,在打cab包之前還會對ocx文件進行代碼簽名。
三、如何進行代碼簽名?
1、首先需準備用於代碼簽名的相關工具SignTool.exe,可以從這裏下載。
2、申請可用於ocx文件簽名的代碼簽名數字證書,一般是需付費的。如果是測試用,可以有幾種方式:
a) 自己創建一個測試簽名證書以及根證書;
b) 可以從www.ca365.com網站申請免費代碼簽名證書或測試證書(應用時需自動導入根證書到用戶系統裏);
c) 借用淘寶的支付寶證書(淘寶對每個支付寶用戶都可以頒發一個數字證書)。
申請數字證書的過程實際上是:
1) 在申請者的計算機上創建一個密鑰對,即一個私鑰和一個公鑰,私鑰保留在申請者計算機內,將公鑰傳送給CA機構;
2) CA機構在通過必要的線下審覈後(測試證書和免費證書相當於沒有審覈過程的),用CA自己的私鑰對申請者的申請信息進行簽名(申請者信息包括申請者傳過來的公鑰、申請者自身基本信息等),並加上時間戳。CA對申請信息進行加密後就生成了一個證書,一般是以.cer文件的形式下發給申請者。
3、用signTool工具對文件進行簽名:
Signtool sign /f "xxxx.pfx" /d "卡設備讀寫機ActiveX" /du http://www.51diancai.com /t http://timestamp.verisign.com/scripts/timstamp.dll "xxxxx.cab"
其中:
xxxx.pfx 是簽名證書(實際上包含了代碼簽名數字證書和私鑰,在IE的證書管理容器裏“個人”裏導出時可以選擇包含私鑰);
http://timestamp.verisign.com/scripts/timstamp.dll 是時間戳服務。
有關代碼簽名以及創建測試簽名證書的問題以下文章均有詳細介紹,不再贅述:
- a) 製作臨時證書爲ActiveX控件簽名
- b) 給控件做數字簽名
- c) 微軟代碼簽名證書使用指南
- d) VeriSign代碼簽名證書技術白皮書_v1.0_090413
- e) VC++開發Activex控件以及簽名發佈
- f) VC2005從開發MFC ActiveX ocx控件到發佈到.net網站的全部過程
- g) ActiveX的數字簽名
- h) A Complete ActiveX Web Control Tutorial – CodeProject
四、根證書自動安裝問題
如果數字簽名證書是從VeriSign等機構購買的,一般不存在根證書問題,因爲微軟的IE已經默認將VeriSign設置爲受信任的根證書機構了:
但如果是從CA365這類不是很權威的機構申請的證書,由於這些機構的根證書默認並不在IE的“受信任的根證書頒發機構”名單裏,所以在使用時需要先在用戶電腦上將CA365的根證書手動導入進去。其實,12306.cn網站的根證書就是自己給自己頒發的,所以其網站上就明文提示需要自動安裝根證書:
對於絕大部分用戶來說,對什麼什麼叫數字簽名什麼叫根證書之類的概念是一頭霧水的,讓他們自己安裝根證書感覺有點憋屈。如果能讓IE自己自動安裝根證書就好了。理論上是可以自動安裝的,但由於權限的問題實際用起來不是那麼爽,用戶體驗仍然很差。其原理就是利用微軟的CAPICOM組件和xenrlinf組件對本地證書進行操作從而達到檢測根證書是否存在以及安裝證書的目的的,以下有兩個文章對此做了描述:
a) 將Capicom調用代碼封裝到ActiveX——解決javascript調Capicom讀取數字證書信息時,IE彈出安全提示的問題
上海證券報社遠程辦公系統就使用了自動安裝根證書的方法:
另外,在CA365上也提供了自動安裝根證書的代碼例子(如何在用戶的客戶機上自動安裝根證書?):
http://www.ca365.com/forward.do?pageurl=/ca/yhsc/11.jsp
只不過這個例子在運行時一般會出錯,以下語句總是創建對象失敗:
Set st = CreateObject("CAPICOM.Store")
究其原因,還是瀏覽器對ActiveX的安全設置問題,降低IE對ActiveX的安全設置就可以了,但這個要求對於用戶來說太麻煩了,還不然讓用戶自己一步一步將根證書導入系統中,或者自己包裝一個安裝程序,運行後自動導入根證書。