WinHTTP 中的驗證
某些 HTTP 伺服器和 Proxy 需要驗證,才能允許存取因特網上的資源。 Microsoft Windows HTTP 服務 (WinHTTP) 函式支援 HTTP 工作階段的伺服器和 Proxy 驗證。
關於 HTTP 驗證
如果需要驗證,HTTP 應用程式會收到狀態代碼 401(伺服器需要驗證)或 407(Proxy 需要驗證)。 除了狀態代碼之外,Proxy 或伺服器會傳送一或多個驗證標頭:WWW-Authenticate(用於伺服器驗證)或 Proxy-Authenticate(用於 Proxy 驗證)。
每個驗證標頭都包含支持的驗證配置,而基本和摘要配置則包含領域。 如果支援多個驗證配置,伺服器會傳回多個驗證標頭。 領域值會區分大小寫,並定義一組接受相同認證的伺服器或 Proxy。 例如,需要伺服器驗證時,可能會傳回標頭 「WWW-Authentication: Basic Realm=」example」。 此標頭指定必須提供「範例」網域的用戶認證。
HTTP 應用程式可以包含授權標頭字段,其中包含傳送至伺服器的要求。 授權標頭包含驗證配置,以及該配置所需的適當回應。 例如,如果用戶端收到回應標頭 「WWW-Authenticate: Basic Realm=」example“,則會將 ”Authorization: Basic <username:password>“ 新增至要求,並傳送至伺服器。
注意
雖然它們會顯示為純文字,但使用者名稱和密碼實際上 base64 編碼。
驗證設定有兩種一般類型:
基本身份驗證配置,其中使用者名稱和密碼會以純文本傳送至伺服器。
基本身份驗證配置是以客戶端必須針對每個領域的使用者名稱和密碼來識別本身的模型為基礎。 只有當要求是以包含有效使用者名稱和密碼的授權標頭傳送時,伺服器才會服務要求。
挑戰回應配置,例如 Kerberos,其中伺服器會挑戰用戶端 驗證資料。 用戶端會使用使用者認證轉換數據,並將轉換的數據傳回伺服器以進行驗證。
挑戰回應配置可啟用更安全的驗證。 在挑戰回應配置中,用戶名稱和密碼永遠不會透過網路傳輸。 用戶端選取挑戰回應配置之後,伺服器會傳回適當的狀態代碼,其中包含該配置 驗證數據。 客戶端接著會以適當的回應重新傳送要求,以取得要求的服務。 挑戰回應配置可能需要多個交換才能完成。
下表包含 WinHTTP 支援的驗證配置、驗證類型,以及配置的描述。
方案 | 類型 | 描述 |
---|---|---|
基本 (純文字) | 基本 | 使用包含使用者名稱和密碼的 base64 編碼 字串。 |
消化 | 挑戰-回應 | 使用 nonce (伺服器指定的數據字串) 值的挑戰。 有效的回應包含使用者名稱、密碼、指定 nonce 值的總和檢查碼、HTTP 動詞,以及要求的統一資源識別碼 (URI)。 |
NTLM | 挑戰-回應 | 需要以使用者認證轉換 驗證數據 來證明身分識別。 若要讓NTLM驗證正常運作,必須在同一個連線上進行數個交換。 因此,如果交錯 Proxy 不支援保持連線,則無法使用NTLM驗證。 如果 WinHttpSetOption 搭配停用 keep-alive 語意的 WINHTTP_DISABLE_KEEP_ALIVE 旗標使用 NTLM 驗證也會失敗。 |
護照 | 挑戰-回應 | 使用 Microsoft Passport 1.4。 |
談判 | 挑戰-回應 | 如果伺服器和用戶端都使用 Windows 2000 或更新版本,則會使用 Kerberos 驗證。 否則會使用NTLM驗證。 Kerberos 可在 Windows 2000 和更新版本的作系統中使用,且被視為比 NTLM 驗證更安全。 若要讓交涉驗證正常運作,必須在同一個連線上進行數個交換。 因此,如果交錯 Proxy 不支援保持連線,就無法使用交涉驗證。 如果 winHttpSetOption 搭配停用keep-alive語意的 WINHTTP_DISABLE_KEEP_ALIVE 旗標使用,交涉驗證也會失敗。 交涉驗證配置有時稱為整合式 Windows 驗證。 |
WinHTTP 應用程式中的驗證
WinHTTP 應用程式開發介面 (API) 提供兩個函式,用於在需要驗證的情況下存取因特網資源:WinHttpSetCredentials 和 WinHttpQueryAuthSchemes。
當收到具有 401 或 407 狀態代碼的回應時,WinHttpQueryAuthSchemes 可用來剖析驗證標頭,以判斷支持的驗證配置和驗證目標。 驗證目標是要求驗證的伺服器或 Proxy。 WinHttpQueryAuthSchemes 也會根據伺服器建議的驗證配置喜好設定,從可用的配置決定第一個驗證配置。 這個選擇驗證配置的方法是 RFC 2616所建議的行為。
WinHttpSetCredentials 可讓應用程式指定驗證配置,以及用於目標伺服器或 Proxy 的有效使用者名稱和密碼。 設定認證並重新傳送要求之後,系統會自動產生必要的標頭並新增至要求。 因為某些驗證配置需要多個交易,WinHttpSendRequest 可能會傳回錯誤,ERROR_WINHTTP_RESEND_REQUEST。 發生此錯誤時,應用程式應該會繼續重新傳送要求,直到收到不包含 401 或 407 狀態代碼的響應為止。 200 狀態代碼表示資源可用且要求成功。 如需可傳回的其他狀態代碼,請參閱 HTTP 狀態代碼。
如果在將要求傳送至伺服器之前已知可接受的驗證配置和認證,應用程式可以在呼叫 winHttpSendRequest之前呼叫 WinHttpSetCredentials。 在此情況下,WinHTTP 會嘗試向伺服器進行預先驗證,方法是在初始要求中提供認證或 驗證數據。 預先驗證可減少驗證程式中的交換數目,因而改善應用程式效能。
預先驗證可以搭配下列驗證設定使用:
- 基本 - 一律可行。
- 談判解決 Kerberos - 極有可能;唯一的例外狀況是用戶端與域控制器之間的時間扭曲關閉時。
- (談判解決 NTLM) - 永遠不可能。
- NTLM - 僅適用於 Windows Server 2008 R2。
- 摘要 - 永遠不可能。
- Passport - 永遠不可能;在初始挑戰響應之後,WinHTTP 會使用 Cookie 預先驗證 Passport。
典型的 WinHTTP 應用程式會完成下列步驟,以處理驗證。
- 使用 WinHttpOpenRequest 和 WinHttpSendRequest要求資源。
- 使用 WinHttpQueryHeaders檢查響應標頭。
- 如果傳回 401 或 407 狀態代碼,表示需要驗證,請呼叫 WinHttpQueryAuthSchemes 尋找可接受的配置。
- 使用 WinHttpSetCredentials設定驗證配置、使用者名稱和密碼。
- 呼叫 WinHttpSendRequest,以相同的要求句柄重新傳送要求。
WinHttpSetCredentials 所設定的認證只會用於一個要求。 WinHTTP 不會快取認證以用於其他要求,這表示必須撰寫可回應多個要求的應用程式。 如果重新使用已驗證的連線,其他要求可能不會受到挑戰,但您的程式代碼應該可以隨時回應要求。
範例:擷取檔
下列範例程式代碼會嘗試從 HTTP 伺服器擷取指定的檔。 狀態代碼是從回應擷取,以判斷是否需要驗證。 如果找到 200 狀態代碼,則會提供檔。 如果找到 401 或 407 的狀態代碼,則需要驗證才能擷取檔。 針對任何其他狀態代碼,會顯示錯誤訊息。 如需可能的狀態代碼清單,請參閱 HTTP 狀態代碼。
#include <windows.h>
#include <winhttp.h>
#include <stdio.h>
#pragma comment(lib, "winhttp.lib")
DWORD ChooseAuthScheme( DWORD dwSupportedSchemes )
{
// It is the server's responsibility only to accept
// authentication schemes that provide a sufficient
// level of security to protect the servers resources.
//
// The client is also obligated only to use an authentication
// scheme that adequately protects its username and password.
//
// Thus, this sample code does not use Basic authentication
// becaus Basic authentication exposes the client's username
// and password to anyone monitoring the connection.
if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_NEGOTIATE )
return WINHTTP_AUTH_SCHEME_NEGOTIATE;
else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_NTLM )
return WINHTTP_AUTH_SCHEME_NTLM;
else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_PASSPORT )
return WINHTTP_AUTH_SCHEME_PASSPORT;
else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_DIGEST )
return WINHTTP_AUTH_SCHEME_DIGEST;
else
return 0;
}
struct SWinHttpSampleGet
{
LPCWSTR szServer;
LPCWSTR szPath;
BOOL fUseSSL;
LPCWSTR szServerUsername;
LPCWSTR szServerPassword;
LPCWSTR szProxyUsername;
LPCWSTR szProxyPassword;
};
void WinHttpAuthSample( IN SWinHttpSampleGet *pGetRequest )
{
DWORD dwStatusCode = 0;
DWORD dwSupportedSchemes;
DWORD dwFirstScheme;
DWORD dwSelectedScheme;
DWORD dwTarget;
DWORD dwLastStatus = 0;
DWORD dwSize = sizeof(DWORD);
BOOL bResults = FALSE;
BOOL bDone = FALSE;
DWORD dwProxyAuthScheme = 0;
HINTERNET hSession = NULL,
hConnect = NULL,
hRequest = NULL;
// Use WinHttpOpen to obtain a session handle.
hSession = WinHttpOpen( L"WinHTTP Example/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0 );
INTERNET_PORT nPort = ( pGetRequest->fUseSSL ) ?
INTERNET_DEFAULT_HTTPS_PORT :
INTERNET_DEFAULT_HTTP_PORT;
// Specify an HTTP server.
if( hSession )
hConnect = WinHttpConnect( hSession,
pGetRequest->szServer,
nPort, 0 );
// Create an HTTP request handle.
if( hConnect )
hRequest = WinHttpOpenRequest( hConnect,
L"GET",
pGetRequest->szPath,
NULL,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
( pGetRequest->fUseSSL ) ?
WINHTTP_FLAG_SECURE : 0 );
// Continue to send a request until status code
// is not 401 or 407.
if( hRequest == NULL )
bDone = TRUE;
while( !bDone )
{
// If a proxy authentication challenge was responded to, reset
// those credentials before each SendRequest, because the proxy
// may require re-authentication after responding to a 401 or
// to a redirect. If you don't, you can get into a
// 407-401-407-401- loop.
if( dwProxyAuthScheme != 0 )
bResults = WinHttpSetCredentials( hRequest,
WINHTTP_AUTH_TARGET_PROXY,
dwProxyAuthScheme,
pGetRequest->szProxyUsername,
pGetRequest->szProxyPassword,
NULL );
// Send a request.
bResults = WinHttpSendRequest( hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS,
0,
WINHTTP_NO_REQUEST_DATA,
0,
0,
0 );
// End the request.
if( bResults )
bResults = WinHttpReceiveResponse( hRequest, NULL );
// Resend the request in case of
// ERROR_WINHTTP_RESEND_REQUEST error.
if( !bResults && GetLastError( ) == ERROR_WINHTTP_RESEND_REQUEST)
continue;
// Check the status code.
if( bResults )
bResults = WinHttpQueryHeaders( hRequest,
WINHTTP_QUERY_STATUS_CODE |
WINHTTP_QUERY_FLAG_NUMBER,
NULL,
&dwStatusCode,
&dwSize,
NULL );
if( bResults )
{
switch( dwStatusCode )
{
case 200:
// The resource was successfully retrieved.
// You can use WinHttpReadData to read the
// contents of the server's response.
printf( "The resource was successfully retrieved.\n" );
bDone = TRUE;
break;
case 401:
// The server requires authentication.
printf(" The server requires authentication. Sending credentials...\n" );
// Obtain the supported and preferred schemes.
bResults = WinHttpQueryAuthSchemes( hRequest,
&dwSupportedSchemes,
&dwFirstScheme,
&dwTarget );
// Set the credentials before resending the request.
if( bResults )
{
dwSelectedScheme = ChooseAuthScheme( dwSupportedSchemes);
if( dwSelectedScheme == 0 )
bDone = TRUE;
else
bResults = WinHttpSetCredentials( hRequest,
dwTarget,
dwSelectedScheme,
pGetRequest->szServerUsername,
pGetRequest->szServerPassword,
NULL );
}
// If the same credentials are requested twice, abort the
// request. For simplicity, this sample does not check
// for a repeated sequence of status codes.
if( dwLastStatus == 401 )
bDone = TRUE;
break;
case 407:
// The proxy requires authentication.
printf( "The proxy requires authentication. Sending credentials...\n" );
// Obtain the supported and preferred schemes.
bResults = WinHttpQueryAuthSchemes( hRequest,
&dwSupportedSchemes,
&dwFirstScheme,
&dwTarget );
// Set the credentials before resending the request.
if( bResults )
dwProxyAuthScheme = ChooseAuthScheme(dwSupportedSchemes);
// If the same credentials are requested twice, abort the
// request. For simplicity, this sample does not check
// for a repeated sequence of status codes.
if( dwLastStatus == 407 )
bDone = TRUE;
break;
default:
// The status code does not indicate success.
printf("Error. Status code %d returned.\n", dwStatusCode);
bDone = TRUE;
}
}
// Keep track of the last status code.
dwLastStatus = dwStatusCode;
// If there are any errors, break out of the loop.
if( !bResults )
bDone = TRUE;
}
// Report any errors.
if( !bResults )
{
DWORD dwLastError = GetLastError( );
printf( "Error %d has occurred.\n", dwLastError );
}
// Close any open handles.
if( hRequest ) WinHttpCloseHandle( hRequest );
if( hConnect ) WinHttpCloseHandle( hConnect );
if( hSession ) WinHttpCloseHandle( hSession );
}
自動登入原則
自動登入(自動登入)原則會決定 WinHTTP 在要求中包含預設認證可接受的時機。 默認認證是目前的線程令牌或會話令牌,取決於 WinHTTP 是否用於同步或異步模式。 線程令牌會以同步模式使用,而會話令牌則用於異步模式。 這些預設認證通常是用來登入 Microsoft Windows 的使用者名稱和密碼。
已實作自動登入原則,以防止這些認證隨便用來對不受信任的伺服器進行驗證。 根據預設,安全性層級會設定為 WINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUM,這可讓預設認證只用於內部網路要求。 自動登入原則僅適用於NTLM和交涉驗證配置。 認證永遠不會與其他配置自動傳輸。
您可以使用 WinHttpSetOption 函式搭配 WINHTTP_OPTION_AUTOLOGON_POLICY 旗標來設定自動登入原則。 此旗標僅適用於要求句柄。 當原則設定為WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW時,默認認證可以傳送至所有伺服器。 當原則設定為WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH時,預設認證無法用於驗證。 強烈建議您在MEDIUM層級使用自動登入。
預存的使用者名稱和密碼
Windows XP 引進了預存使用者名稱和密碼的概念。 如果使用者的 Passport 認證是透過 Passport 註冊精靈 或標準 認證對話框儲存,則會儲存在預存的使用者名稱和密碼中。 在 Windows XP 或更新版本上使用 WinHTTP 時,如果未明確設定認證,WinHTTP 會自動使用預存使用者名稱和密碼中的認證。 這類似於 NTLM/Kerberos 的預設登入認證支援。 不過,使用預設 Passport 認證不受自動登入原則設定的約束。