以遠程計算機上的用戶身份訪問Com+應用
DELPHI程序員開發com+應用的速度是非常快的,其主要原因是其較好地封裝了com+的windows底層功能,開發人員通過較為簡單的類繼承就避開了復雜的com+底層技術細節,使開發人員將精力放在應用本身的功能上面。Delphi在封裝com+應用時采取了許多折衷,在保留通用性的同時也避開了一些實現起來困難但是應用面不太廣的com+底層特性。這些避開的特性中最令delphi com開發人員關心的就是安全特性。從delphi 5開始,有許多人都面臨過這樣的問題:com應用開發出來並且在本機上運行一切正常,但是一旦分發出去實施遠程訪問時,就無法正常運行了。我自己有段時間在看到“拒絕訪問”錯誤提示時會本能的頭暈。其實認真追究起來,還是因為自己對windows安全技術了解不多造成的。多年來我一直沒有發現國內有windows安全方面比較系統的資料和書籍,直到Keith Brownr的<windows安全性編程>中文版的出現。正是基於這本書我才有了下面的一些試驗,也知道了為什麼我老是補拒絕的原因。下面的討論只是我在解決自身現有代碼的安全訪問問題時,總結出的幾個小經難方法。建議願意了解windows安全性的朋友去看一看<windows安全性編程>一書,你會發現windows的安全不再神秘。 這篇文章將會說明如何以遠程工作站上的用戶身份激活com+對象,並以此用戶身份訪問Interface。 1、 Delphi默認com+對象的遠程激活
Delph中遠程com+對象激活一般通過TdispatchConnection及其子類來實現,實際代碼中多用TDCOMConnection或TsocketConnectoion這兩個組件,TDCOMConnection組件最終調用CoCreateInstanceEx創建com+對象。CoCreateInstanceEx (const clsid: TCLSID; unkOuter: IUnknown; dwClsCtx: Longint; ServerInfo:PCoServerInfo;dwCount: Longint; rgmqResults: PMultiQIArray): HResult。 TDCOMConnection在調用CoCreateInstanceEx時為pCoServerInfo參數中的pAuthInfo傳遞了Null值,因此TdcomConnection在創建Com對象時使用的是本地計算機登錄者的用戶令牌。假若A計算機上的登錄用戶Auser使用TDCOMConnection類連接遠程計算機B上的com+對象,則B計算機會使用Auser的用戶名/密碼在B計算機上建立登錄會話並最終創建com+對象。但是一台windows工作站上的本地用戶只能在本地登錄而無法在別的計算機上登錄,因此A計算機上的Auser就無法在B工作站上建立登錄會話,當然也就無法創建com+對象,此時遠程工作站B會嘗試用Guest帳戶建立會話並使用該賬戶激活com+對象。在這種情況下,如果B工作站上的Guest賬戶沒有啟用或Guest沒有激活com+對象的權限,你就會看見令人頭暈的提示“拒絕訪問”。看到這裡你是不對現在網上最“流行”的dcom配置方法有所悟了呢。那個方法就是允許everyone訪問、激活com對象、並且將“默認身份驗證級別”設置成無。這種方法能夠使你的com應用可以“用了”,但是,它可以上“任何人”訪問。而且這種設置你將無法利用com+基於角色的安全訪問控制功能。 2、怎樣不用GUEST賬戶激活 這個問題的實際上應該是:怎樣用遠程工作站上的用戶激活遠程com對象。解決這個問題其實很簡單:只要你在調用CoCreateInstanceEx時為它指定遠程工作站上的用戶名和密碼,只要用戶名/密碼通過遠程計算機的驗證,並且該用戶被授予了“遠程激活”com+對象的權限,那麼遠程工作站會用該用戶身份激活com+對象。看一下代碼:
var
mts:IMTSXjpimsDB;
ov:Variant;
i:integer;
cai:_CoAuthInfo;
cid:_CoAuthIdentity;
csi:COSERVERINFO;
mqi:MULTI_QI;
iid_unk:TGUID;
idsp:IDispatch;
wUser,wDomain,wPsw:WideString;
begin
wUser:=eduser.text;//用戶名
wDomain:=edSvr.Text;//遠程計算機名
wPsw:=edPsw.Text;//密碼
cid.user:=pUnshort(@wUser[1]);
cid.UserLength:=length(wUser);
cid.Domain:=pUnshort(@wDomain[1]);
cid.DomainLength:=length(wDomain);
cid.password:=pUnshort(@wPsw[1]);
cid.PasswordLength:=length(wPsw);
cid.Flags:=2;
//以上填充_CoAuthIdentity結構
cai.dwAuthnSvc:=10;//winNt默認的鑒證服務
cai.dwAuthzSvc:=0;
cai.pwszServerPrincName:=wDomain;
cai.dwAuthnLevel:=0;
cai.dwImpersonationLevel:=3;//必須設置成模擬
cai.pAuthIdentityData:=@cid;
cai.dwCapabilities:=$0800; //以上填充_CoAuthInfo結構
FillChar(csi, sizeof(csi), 0);
csi.dwReserved1:=0;
csi.pwszName:=pwidechar(wdomain);
csi.pAuthInfo:=@cai;
//以上填充COSERVERINFO結構
iid_unk:=IUnknown;
mqi.IID:=@iid_unk;mqi.Itf:=nil;mqi.hr:=0;
Screen.Cursor:=crHourGlass; olecheck(CoCreateInstanceEx(CLASS_MTSXjpimsDB,nil,CLSCTX_REMOTE_SERVER,@csi,1,@mqi)); 這段代碼中除了最後實際調用CoCreateInstanceEx外,前面的代碼都是設置參數。這些參數的含義請大家參考msdn,除了用戶名、主機名、密碼外,只有一個重要要部分要說明:cai.dwImpersonationLevel必須設置成允許模擬(值為3),否則遠程計算機將無法按提供的用戶/密碼建議網絡會話。 3、不修改現有代碼,可以實現用遠程用戶身份激活嗎? 當然可以,我擴展了TDcomConnection類,為其加入了用戶名和密碼,並修改其默認的DoConnect方法,使其在調用CoCreateInstanceEx時用指定的用戶名和密碼填充參數。代碼如下:
unit SecDComConnection;
interface
uses
windows,SysUtils, Classes,ActiveX, DB, DBClient, MConnect,comobj,Midas;
type
{typedef struct _SEC_WINNT_AUTH_IDENTITY
unsigned short __RPC_FAR* User;
unsigned long UserLength;
unsigned short __RPC_FAR* Domain;
unsigned long DomainLength;
unsigned short __RPC_FAR* Password;
unsigned long PasswordLength;
unsigned long Flags;
SEC_WINNT_AUTH_IDENTITY, *PSEC_WINNT_AUTH_IDENTITY;
}
{typedef struct _COAUTHIDENTITY
USHORT * User;
ULONG UserLength;
USHORT * Domain;
ULONG DomainLength;
USHORT * Password;
ULONG PasswordLength;
ULONG Flags;
COAUTHIDENTITY;}
{#define RPC_C_AUTHN_NONE 0
#define RPC_C_AUTHN_DCE_PRIVATE 1
#define RPC_C_AUTHN_DCE_PUBLIC 2
#define RPC_C_AUTHN_DEC_PUBLIC 4
#define RPC_C_AUTHN_GSS_NEGOTIATE 9
#define RPC_C_AUTHN_WINNT 10
#define RPC_C_AUTHN_GSS_SCHANNEL 14
#define RPC_C_AUTHN_GSS_KERBEROS 16
#define RPC_C_AUTHN_MSN 17
#define RPC_C_AUTHN_DPA 18
#define RPC_C_AUTHN_MQ 100
#define RPC_C_AUTHN_DEFAULT 0xFFFFFFFFL
}
{#define RPC_C_AUTHZ_NONE 0
#define RPC_C_AUTHZ_NAME 1
#define RPC_C_AUTHZ_DCE 2
#define RPC_C_AUTHZ_DEFAULT 0xFFFFFFFF }
{
#define RPC_C_AUTHN_LEVEL_DEFAULT 0
#define RPC_C_AUTHN_LEVEL_NONE 1
#define RPC_C_AUTHN_LEVEL_CONNECT 2
#define RPC_C_AUTHN_LEVEL_CALL 3
#define RPC_C_AUTHN_LEVEL_PKT 4
#define RPC_C_AUTHN_LEVEL_PKT_INTEGRITY 5
#define RPC_C_AUTHN_LEVEL_PKT_PRIVACY 6 }
{SEC_WINNT_AUTH_IDENTITY_UNICODE=2 }
pUnShort=^Word;
pCoAuthIdentity=^_CoAuthIdentity;
_CoAuthIdentity=record
user:pUnShort;
UserLength:ULONG;
Domain:pUnShort;
DomainLength:Ulong;
password:pUnShort;
PasswordLength:ulong;
Flags:ulong;
end;
_CoAuthInfo=record
dwAuthnSvc:DWORD;
dwAuthzSvc:DWORD;
pwszServerPrincName:WideString;
dwAuthnLevel:Dword;
dwImpersonationLevel:dword;
pAuthIdentityData:pCoAuthIdentity;
dwCapabilities:DWORD;
end;
TSecDComConnection = class(TDCOMConnection)
private
FCai:_CoAuthInfo;
FCid:_CoAuthIdentity;
FSvInfo:COSERVERINFO;
FUser:WideString;
FPassWord:WideString;
procedure SetPassword(const Value: wideString);
procedure SetUser(const Value: wideString);
procedure SetSvInfo(const Value: COSERVERINFO);
protected
procedure DoConnect; override;
public
property SvInfo:COSERVERINFO read FSvInfo write SetSvInfo;
constructor Create(AOwner: TComponent); override;
procedure MySetBlanket(itf:IUnknown;const vCai:_CoAuthInfo);
function GetServer: IAppServer; override;
published
property User:wideString read FUser write SetUser;
Property Password:wideString read FPassword write SetPassword;
end;
procedure Register;
implementation
constructor TSecDCOMConnection.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FillMemory(@Fcai,sizeof(Fcai),0);
FillMemory(@FCid,sizeof(FCid),0);
FillMemory(@FSvInfo,sizeof(FSvInfo),0);
with FCai do begin
dwAuthnSvc:=10;//RPC_C_AUTHN_WINNT
dwAuthzSvc:=0;// RPC_C_AUTHZ_NONE
dwAuthnLevel:=0;//RPC_C_AUTHN_LEVEL_DEFAULT
dwImpersonationLevel:=3;
pAuthIdentityData:=@fcid;
dwCapabilities:=$0800;
end;
end;
procedure TSecDCOMConnection.DoConnect;
var
tmpCmpName:widestring;
IID_IUnknown:TGUID;
iiu:IDispatch;
Mqi:MULTI_QI;
qr:HRESULT;
begin
if (ObjectBroker) <> nil then
begin
repeat
if ComputerName = '' then
ComputerName := ObjectBroker.GetComputerForGUID(GetServerCLSID);
try
SetAppServer(CreateRemoteComObject(ComputerName, GetServerCLSID) as IDispatch);
ObjectBroker.SetConnectStatus(ComputerName, True);
except
ObjectBroker.SetConnectStatus(ComputerName, False);
ComputerName := '';
end;
until Connected;
end
else if (ComputerName <> '') then
begin
with fcid do begin
user:=pUnshort(@fuser[1]);
UserLength:=length(fuser);
tmpCmpName:=ComputerName;
Domain:=pUnshort(@tmpCmpName[1]);
DomainLength:=length(TmpCmpName);
password:=pUnShort(@FPassword[1]);
PasswordLength:=length(FPassword);
Flags:=2;//Unicode
end;
FSvInfo.pwszName:=pwidechar(tmpCmpName);
FSvinfo.pAuthInfo:=@Fcai;
IID_IUnknown:=IUnknown;
mqi.IID:=@IID_IUnknown;mqi.Itf:=nil;mqi.hr:=0;
olecheck(CoCreateInstanceEx(GetServerCLSID,nil,CLSCTX_REMOTE_SERVER,@FSvinfo,1,@mqi));
olecheck(mqi.hr);
MySetBlanket(mqi.Itf,Fcai);
qr:=mqi.Itf.QueryInterface(idispatch,iiu);
olecheck(qr);
MySetBlanket(IUnknown(iiu),FCai);
SetAppServer(iiu);
end
else
inherited DoConnect;
end;
function TSecDComConnection.GetServer: IAppServer;
var
QIResult: HResult;
begin
Connected := True;
QIResult := IDispatch(AppServer).QueryInterface(IAppServer, Result);
if QIResult <> S_OK then
begin
Result := TDispatchAppServer.Create(IAppServerDisp(IDispatch(AppServer)));
end;
MySetBlanket(IUnknown(Result),FCai);
end;
procedure TSecDCOMConnection.MySetBlanket(itf: IUnknown;
const vCai: _CoAuthInfo);
begin
with vCai do
CoSetProxyBlanket(Itf,dwAuthnSvc,dwAuthzSvc,pwidechar(pAuthIdentityData^.Domain),
dwAuthnLevel,dwImpersonationLevel,pAuthIdentityData,dwCapabilities);
end;
procedure TSecDCOMConnection.SetPassword(const Value: wideString);
begin
FPassword := Value;
end;
procedure TSecDCOMConnection.SetSvInfo(const Value: COSERVERINFO);
begin
FSvInfo := Value;
end;
procedure TSecDCOMConnection.SetUser(const Value: wideString);
begin
FUser := Value;
end;
procedure Register;
begin
RegisterComponents('DataSnap', [TSecDComConnection]);
end;
end.
代碼中有一些C風格的注釋,是因為delphi沒有為我們預定義這些變量和數據結構。 如何使用呢?將這個組件安裝在IDE中,並將其放到你的現有代碼的遠程數據模塊中去,將原有指向TDOCMConnection的數據集控件設置成這個新的TSecDCOMConnection控件。然後你可以在遠程計算機中設置最嚴格的安全選項。但是要記住應該為你要使用的用戶設置合適的權限:給予遠程激活權限、給予遠程訪問權限。 4、到現在還沒有談到訪問的問題。首先激活和訪問並不是一回事。一個用戶可能擁有激活權限但沒有訪問權限,也有可能只有訪問權限卻無激活權限。前面談到CoCreateInstacnceEx可以用另一身份激活對象並取得IunKnown指針的一個本地引用。如果你直接用這個指針去取得IappServer接口並調用方法,那麼你很可能又會見到“拒絕訪問”信息。這是IUnKnown指針的本地引用存在於客戶機的進程中,再沒有做特殊設置前,該指針繼承了客戶機進程的本地令牌,也就是說當用這個指針獲取遠程IappServer接口時,會用客戶機當前登錄令牌調用QueryInterface,在調用過程中遠程計算機將有此令牌中緩存的用戶名和密碼進行再次登錄驗證,當然此時又會被拒絕,而後遠程計算機再次嘗試用GUEST帳戶登錄並獲取com對象接口,此時若沒有找開GUEST訪問權限,則客戶端訪問失敗,windows返回“拒絕訪問”信息。那麼怎樣才能使QueryInterface調用也使用遠程用戶身份呢,這就要調用CoSetProxyBlanket強制設置本地接口引用使用遠程用戶的令牌。在上面的代碼中,我用MySetBlanket包裝了該API,以便使用激活時的用戶身份調用QueryInterface。而後在取得的IappServer接口上再次調用MySetBlanket,保證在使用該接口時也采用遠程用戶身份。
MySetBlanket(mqi.Itf,Fcai);
qr:=mqi.Itf.QueryInterface(idispatch,iiu);
olecheck(qr);
MySetBlanket(IUnknown(iiu),FCai); 為保證直接引用DataProvider的TclientDataSet也能按上述要求工作,在擴展的TSecDCOMConnection控件中,重載了GetServer方法。這樣TSecDCOMConnection已能完全替換TDCOMConnection實現便利的com+應用編程了。 由於時間倉促,寫這篇時很多術語沒有做解釋交待,因此可能會有一些不太好理解,但是出於為delphi Fans提供一個簡單的實現安全性com訪問的方法,我還是將這篇貼上來,主要是可以讓需要的朋友直接復制代碼用在自己的應用上。使用TSecDCOMConnection後,服務器方的com+對象可以強制找開訪問檢查,並打開組件級的訪問檢查。在打開訪問檢查的情況下,必須將服務器中允許訪問com+對象的用戶名加入到角色中才能正確訪問。 (上述代碼在delphi7/winXP sp2中調試通過,對於windows98和windows nt4.0及以下操作系統,由於CoCreateInstanceEx不能直接生成com+對象的安全上下文,因此代碼不可用)
昆侖踏月 於烏魯木齊 2005.6.18