標籤:des io os 使用 ar for 資料 sp 問題
DELPHI程式員開發com+應用的速度是非常快的,其主要原因是其較好地封裝了com+的window
s底層功能,開發人員通過較為簡單的類繼承就避開了複雜的com+底層技術細節,使開發人員
將精力放在應用本身的功能上面。Delphi在封裝com+應用時採取了許多折衷,在保留通用性
的同時也避開了一些實現起來困難但是應用面不太廣的com+底層特性。這些避開的特性中最
令delphi com開發人員關心的就是安全特性。從delphi 5開始,有許多人都面臨過這樣的問
題:com應用開發出來並且在本機上運行一切正常,但是一旦分發出去實施遠端存取時,就無
法正常運行了。我自己有段時間在看到“拒絕訪問”錯誤提示時會本能的頭暈。其實認真追
究起來,還是因為自己對windows安全技術瞭解不多造成的。多年來我一直沒有發現國內有w
indows安全方面比較系統的資料和書籍,直到Keith Brownr的<windows安全性編程>中文版的
出現。正是基於這本書我才有了下面的一些實驗,也知道了為什麼我老是補拒絕的原因。下
面的討論只是我在解決自身現有代碼的安全訪問問題時,總結出的幾個小經難方法。建議願
意瞭解windows安全性的朋友去看一看<windows安全性編程>一書,你會發現windows的安全不
再神秘。這篇文章將會說明如何以遠程工作站上的使用者身份啟用com+對象,並以此使用者身份
訪問Interface。 1、 Delphi預設com+對象的遠程啟用
Delph中遠程com+對象啟用一般通過TdispatchConnection及其子類來實現,實際代碼中
多用TDCOMConnection或TsocketConnectoion這兩個組件,TDCOMConnection組件最終調用Co
CreateInstanceEx建立com+對象。CoCreateInstanceEx (const clsid: TCLSID; unkOuter:
IUnknown; dwClsCtx: Longint; ServerInfo:PCoServerInfo;dwCount: Longint; rgmqRes
ults: PMultiQIArray): HResult。 TDCOMConnection在調用CoCreateInstanceEx時為pCoSe
rverInfo參數中的pAuthInfo傳遞了Null值,因此TdcomConnection在建立Com對象時使用的是
本機電腦登入者的使用者令牌。假若A電腦上的登入使用者Auser使用TDCOMConnection類串連
遠端電腦B上的com+對象,則B電腦會使用Auser的使用者名稱/密碼在B電腦上建立登入工作階段
並最終建立com+對象。但是一台windows工作站上的本機使用者只能在本地登入而無法在別的計
算機上登入,因此A電腦上的Auser就無法在B工作站上建立登入工作階段,當然也就無法建立c
om+對象,此時遠程工作站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:[email protected];
cai.dwCapabilities:=$0800; //以上填充_CoAuthInfo結構
FillChar(csi, sizeof(csi), 0);
csi.dwReserved1:=0;
csi.pwszName:=pwidechar(wdomain);
csi.pAuthInfo:[email protected];
//以上填充COSERVERINFO結構
iid_unk:=IUnknown;
mqi.IID:[email protected]_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:[email protected];
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:[email protected];
IID_IUnknown:=IUnknown;
mqi.IID:[email protected]_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指標的本地引用存在於客戶機的進程中,再沒有做特
殊設定前,該指標繼承了客戶機進程的本地令牌,也就是說當用這個指標擷取遠程IappServ
er介面時,會用客戶機當前登入令牌調用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的TclientD
ataSet也能按上述要求工作,在擴充的TSecDCOMConnection控制項中,重載了GetServer方法。
這樣TSecDCOMConnection已能完全替換TDCOMConnection實現便利的com+應用編程了。由於時
間倉促,寫這篇時很多術語沒有做解釋交待,因此可能會有一些不太好理解,但是出於為de
lphi Fans提供一個簡單的實現安全性com訪問的方法,我還是將這篇貼上來,主要是可以讓
需要的朋友直接複製代碼用在自己的應用上。使用TSecDCOMConnection後,伺服器方的com+
對象可以強制找開訪問檢查,並開啟組件級的訪問檢查。在開啟訪問檢查的情況下,必須將
伺服器中允許訪問com+對象的使用者名稱加入到角色中才能正確訪問。 (上述代碼在delphi7/wi
nXP sp2中調試通過,對於windows98和windows nt4.0及以下作業系統,由於CoCreateInsta
nceEx不能直接產生com+對象的安全上下文,因此代碼不可用)
以遠端電腦上的使用者身份訪問Com+應用