WinHTTP 中的驗證

某些 HTTP 伺服器和 Proxy 需要驗證,才能允許存取網際網路上的資源。 Microsoft Windows HTTP Services (WinHTTP) 函式支援 HTTP 會話的伺服器和 Proxy 驗證。

關於 HTTP 驗證

如果需要驗證,HTTP 應用程式會收到狀態碼 401 (伺服器需要驗證) 或 407 (Proxy 需要驗證) 。 除了狀態碼之外,Proxy 或伺服器會傳送一或多個驗證標頭:WWW-Authenticate (伺服器驗證) 或 proxy 驗證Proxy-Authenticate () 。

每個驗證標頭都包含支援的驗證配置,而基本和摘要配置則為領域。 如果支援多個驗證配置,伺服器會傳回多個驗證標頭。 領域值會區分大小寫,並定義一組接受相同認證的伺服器或 Proxy。 例如,當需要伺服器驗證時,可能會傳回標頭 「WWW-Authentication: Basic Realm=」example」。 此標頭指定必須提供「範例」網域的使用者認證。

HTTP 應用程式可以包含授權標頭欄位,並要求傳送至伺服器。 授權標頭包含驗證配置,以及該配置所需的適當回應。 例如,如果用戶端收到回應標頭 「WWW-Authenticate: Basic Realm=」example「,則會將 」Authorization: Basic < username:password > 「 標頭新增至要求,並傳送至伺服器。

注意

雖然它們會顯示為純文字,但使用者名稱和密碼實際上是 base64 編碼

 

驗證配置有兩種一般類型:

  • 基本驗證配置,其中使用者名稱和密碼會以純文字傳送至伺服器。

    基本驗證配置是以用戶端必須針對每個領域的使用者名稱和密碼來識別本身的模型為基礎。 只有當要求是以包含有效使用者名稱和密碼的授權標頭傳送時,伺服器才會服務要求。

  • 挑戰回應配置,例如 Kerberos,其中伺服器會利用 驗證資料來挑戰用戶端。 用戶端會使用使用者認證轉換資料,並將轉換的資料傳回伺服器以進行驗證。

    挑戰回應配置可啟用更安全的驗證。 在挑戰-回應配置中,使用者名稱和密碼永遠不會透過網路傳輸。 用戶端選取挑戰回應配置之後,伺服器會傳回適當的狀態碼,其中包含該配置 之驗證資料 的挑戰。 然後,用戶端會以適當的回應重新傳送要求,以取得要求的服務。 挑戰回應配置可能需要多個交換才能完成。

下表包含 WinHTTP 支援的驗證配置、驗證類型,以及配置的描述。

配置 類型 Description
基本 (純文字) 基本 使用包含使用者名稱和密碼的 base64 編碼 字串。
Digest 挑戰回應 使用 nonce (伺服器指定的資料字串) 值的挑戰。 有效的回應包含使用者名稱、密碼、指定的 nonce 值、 HTTP 動詞,以及要求的統一資源識別項 (URI) 總和檢查碼。
NTLM 挑戰回應 需要使用使用者認證來轉換 驗證資料 ,才能證明身分識別。 若要讓 NTLM 驗證正常運作,必須在相同的連線上進行數個交換。 因此,如果交錯 Proxy 不支援保持連線,則無法使用 NTLM 驗證。 如果 WinHttpSetOption 搭配停用保持運作語意的 WINHTTP_DISABLE_KEEP_ALIVE 旗標使用,NTLM 驗證也會失敗。
Passport 挑戰回應 使用 Microsoft Passport 1.4
交涉 挑戰回應 如果伺服器和用戶端都使用 Windows 2000 或更新版本,則會使用 Kerberos 驗證。 否則會使用 NTLM 驗證。 Kerberos 可在 Windows 2000 和更新版本的作業系統中使用,並被視為比 NTLM 驗證更安全。 若要讓交涉驗證正常運作,必須在相同的連線上進行數個交換。 因此,如果交錯 Proxy 不支援保持連線,就無法使用交涉驗證。 如果 WinHttpSetOption 與停用保持運作語意 的WINHTTP_DISABLE_KEEP_ALIVE 旗標搭配使用,交涉驗證也會失敗。 交涉驗證配置有時稱為整合式Windows 驗證。

 

WinHTTP 應用程式中的驗證

WinHTTP 應用程式程式設計介面 (API) 提供兩個函式,可用來存取需要驗證的網際網路資源: WinHttpSetCredentialsWinHttpQueryAuthSchemes

當收到具有 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 應用程式會完成下列步驟,以處理驗證。

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 認證不受自動登入原則設定限制。