一些評論,然後一些工作代碼片段。
首先,在做Kerberos時,它真的有助於瞭解SPN是什麼,以及它爲什麼重要。一旦你在腦海中有了一個清晰的想法是什麼,那麼其餘的很多事情會讓你更有意義。
簡而言之,Kerberos身份驗證本質上是客戶端計算機,服務器計算機和KDC(域控制器)之間的三方會話。在一般情況下,客戶端不太瞭解服務器應用程序在服務器機器上的配置(也不需要) - 特別是它不知道服務器應用程序正在運行的用戶帳戶。但對於Kerberos來說,瞭解這一點非常重要 - 客戶端本質上需要向KDC詢問可以傳遞給服務器計算機的一些數據。並且服務器機器可以使用此數據blob(再次通過聯繫KDC)來創建運行RPC調用的安全上下文。但是這個數據blob只能通過在正確的用戶帳戶下運行的進程在KDC上兌換。
那麼問題就出現了 - 客戶端如何知道服務器進程正在運行的帳戶。這是SPN進來的地方 - 我想你可以把它看作是存儲在客戶端進程可以指定的KDC/DC中的暱稱。例如,假設你想驗證到一個HTTP服務器 - 一個SPN可能是這樣的形式:
http/hostname.mydomain.com
這本身隻字未提HTTP服務器在其下運行的用戶帳戶,但在域控制器可以在活動目錄中查找並找出http服務器運行的真實帳戶。
爲了獲得所有這些工作,服務器端通常需要在啓動時註冊SPN。我應該注意到,通常使用DsRegisterServerSpn()函數執行此操作,但通常幾乎所有用戶帳戶都沒有足夠的權限來執行此操作。一個例外是LocalSystem帳戶 - 因此,如果您的RPC服務器作爲Windows服務運行,它將能夠註冊SPN。請注意,域管理員可以爲任何帳戶註冊SPN。
如果您無法註冊SPN,則客戶端可以簡單地使用[email protected],其中這是運行RPC服務器的帳戶的用戶名。
現在,如何使所有這與RPC工作。假設您有一個通過套接字進行通信的RPC服務器。服務器端代碼初始化事情會是這個樣子:
#define TCPPORT "1234"
int rpcstart(void)
{
RPC_STATUS status;
unsigned char * pszSecurity = (unsigned char *) NULL;
unsigned int cMinCalls = 1;
unsigned int cMaxCalls = RPC_C_LISTEN_MAX_CALLS_DEFAULT;
RPC_BINDING_VECTOR *pBindingVector;
RPC_CSTR pSpn;
status = RpcServerUseProtseqEp((RPC_CSTR) "ncacn_ip_tcp", cMaxCalls, (RPC_CSTR) TCPPORT, pszSecurity); // Security descriptor
if (status)
{
fprintf(outfile, "RpcServerUseProtseqEp failed\n");
return status;
}
status = RpcServerInqBindings(&pBindingVector);
if (status) {
printf("Failed RpcServerInqBindings\n");
exit(status);
}
status = RpcEpRegister(MyRemote_ServerIfHandle, pBindingVector, NULL, (RPC_CSTR) "build master remote");
if (status) {
printf("Failed RpcEpRegister\n");
exit(status);
}
status = RpcServerRegisterIf(MyRemote_ServerIfHandle, // interface to register
NULL, // MgrTypeUuid
NULL); // MgrEpv; null means use default
if (status)
{
fprintf(outfile, "RpcServerRegisterIf failed\n");
return status;
}
//
// Register "remote/<hostname>" as a SPN. Note that this call will fail
// for normal user accounts as they typically do not have permissions to add
// an SPN. But for the computer account (i.e. running as a local service)
// it will work.
//
// Failure code is usually ERROR_DS_INSUFF_ACCESS_RIGHTS if you aren't a computer
// account (i.e. a service).
//
// Note that if one does this during service startup, one should also clean up
// afterwards during service shutdown (use DS_SPN_DELETE_SPN_OP).
//
status = DsServerRegisterSpn(DS_SPN_ADD_SPN_OP,"remote",NULL);
if(status)
{
//
// If we did not have permissions to register a new SPN, then
// use whatever the default would be. Typically it would be:
//
// [email protected]
//
status = RpcServerInqDefaultPrincName(RPC_C_AUTHN_GSS_KERBEROS, &pSpn);
if(status)
{
fprintf(outfile, "RpcServerInqDefaultPrincName failed\n");
return status;
}
fprintf(outfile, "SPN is %s\n", pSpn);
}
else
{
//
// For our purposes here, this is good enough.
//
pSpn = (RPC_CSTR) "remote/localhost";
}
status = RpcServerRegisterAuthInfo(pSpn, RPC_C_AUTHN_GSS_KERBEROS, NULL, NULL);
if(status)
{
fprintf(outfile, "RpcServerRegisterAuthInfo failed\n");
return status;
}
status = RpcServerListen(cMinCalls, cMaxCalls, TRUE); /* Return immediately */
if (status)
{
fprintf(outfile, "RpcServerListen failed\n");
return status;
}
status = RpcMgmtWaitServerListen(); // wait operation
if (status)
{
fprintf(outfile, "RpcMgmtWaitServerListen failed\n");
return status;
}
return 0;
}
現在,客戶端需要這樣的:
BOOL MyInitRemoteRPC(const char * hostname, int port, const char * spn)
{
RPC_STATUS status;
unsigned sec_options = 0;
DWORD cbSPN = MAX_PATH; char szSPN[MAX_PATH + 1];
char Endpoint[100];
sprintf(Endpoint, "ncacn_ip_tcp:%s[%d]", hostname, port);
/* First create a valid (incomplete) binding handle */
status = RpcBindingFromStringBinding((RPC_CSTR) Endpoint, &MyRemote_IfHandle);
if (status)
{
fprintf(stderr, "Failed to calculate binding\n");
return FALSE;
}
//
// If no SPN is passed in, we assume this to mean that the RPC server was
// running under the LocalSystem account, and was able to register the SPN
// of the form "remote/hostname".
//
// For cases where no "remote/hostname" SPN was registered, one can always just
// supply "<username>@<domain>" - for example "[email protected]". This can be useful
// when the client/server is being tested outside of the service framework.
//
if(spn == NULL) {
status = DsMakeSpn("remote", hostname, NULL, 0, NULL, &cbSPN, szSPN);
if(status)
{
printf("DsMakeSpn failed\n");
exit(1);
}
spn = szSPN;
}
status = RpcBindingSetAuthInfo(MyRemote_IfHandle,
(RPC_CSTR) spn,
sec_options,
RPC_C_AUTHN_GSS_KERBEROS,
NULL, 0);
if (status) {
printf ("RpcBindingSetAuthInfo failed: 0x%x\n", status);
exit (1);
}
return TRUE;
}
注意這裏是您可以使用各種認證級別 - 見sec_options標誌 傳遞到客戶端的RpcBindingSetAuthInfo()。