WinHTTP 中的身份验证

某些 HTTP 服务器和代理在允许访问 Internet 上的资源之前需要身份验证。 Microsoft Windows HTTP 服务(WinHTTP)函数支持 HTTP 会话的服务器和代理身份验证。

关于 HTTP 身份验证

如果需要身份验证,HTTP 应用程序将收到状态代码 401(服务器需要身份验证)或 407(代理需要身份验证)。 除了状态代码,代理或服务器还发送一个或多个身份验证标头:WWW-Authenticate(用于服务器身份验证)或 Proxy-Authenticate(用于代理身份验证)。

每个身份验证标头都包含受支持的身份验证方案,对于基本和摘要方案,一个领域。 如果支持多个身份验证方案,服务器将返回多个身份验证标头。 领域值区分大小写,并定义一组接受相同凭据的服务器或代理。 例如,当需要服务器身份验证时,可能会返回标头“WWW-Authenticate: Basic Realm=”example”。 此标头指定必须为“example”域提供用户凭据。

HTTP 应用程序可以包含授权标头字段,其中包含发送到服务器的请求。 授权标头包含身份验证方案和该方案所需的相应响应。 例如,如果客户端收到响应标头“WWW-Authenticate: Basic Realm=”example“,则标头”Authorization: Basic <username:password>“将添加到请求并发送到服务器。

注意

虽然它们显示为纯文本,但用户名和密码实际上 base64 编码

 

有两种常规类型的身份验证方案:

  • 基本身份验证方案,其中用户名和密码以明文形式发送到服务器。

    基本身份验证方案基于客户端必须为每个领域标识自己的用户名和密码的模型。 仅当请求使用包含有效用户名和密码的授权标头发送时,服务器才会为请求提供服务。

  • 质询响应方案(如 Kerberos),其中服务器使用 身份验证数据来质询客户端。 客户端使用用户凭据转换数据,并将转换后的数据发送回服务器进行身份验证。

    质询-响应方案可实现更安全的身份验证。 在质询响应方案中,用户名和密码永远不会通过网络传输。 客户端选择质询响应方案后,服务器将返回一个适当的状态代码,其中包含该方案的 身份验证数据。 然后,客户端使用适当的响应重新发送请求以获取请求的服务。 挑战响应方案可能需要多个交换才能完成。

下表包含 WinHTTP 支持的身份验证方案、身份验证类型和方案说明。

方案 类型 描述
基本(纯文本) 基本 使用包含用户名和密码的 base64 编码 字符串。
消化 挑战响应 使用 nonce(服务器指定的数据字符串)值的挑战。 有效的响应包含用户名、密码、给定 nonce 值、HTTP 谓词和请求的统一资源标识符(URI)的校验和。
NTLM 挑战响应 需要使用用户凭据转换 身份验证数据,以证明身份。 若要使 NTLM 身份验证正常工作,必须在同一连接上进行多个交换。 因此,如果干预代理不支持保持连接,则无法使用 NTLM 身份验证。 如果 WinHttpSetOption 与禁用保持活动语义的 WINHTTP_DISABLE_KEEP_ALIVE 标志一起使用,NTLM 身份验证也会失败。
护照 挑战响应 使用 Microsoft Passport 1.4
谈判 挑战响应 如果服务器和客户端都使用 Windows 2000 或更高版本,则使用 Kerberos 身份验证。 否则使用 NTLM 身份验证。 Kerberos 在 Windows 2000 及更高版本的作系统中可用,被认为比 NTLM 身份验证更安全。 若要使协商身份验证正常工作,必须在同一连接上进行多个交换。 因此,如果干预代理不支持保持连接,则无法使用协商身份验证。 如果 WinHttpSetOption 与禁用保持连接语义的 WINHTTP_DISABLE_KEEP_ALIVE 标志一起使用,协商身份验证也会失败。 协商身份验证方案有时称为集成 Windows 身份验证。

 

WinHTTP 应用程序中的身份验证

WinHTTP 应用程序编程接口(API)提供两个函数,用于在需要身份验证的情况下访问 Internet 资源:WinHttpSetCredentialsWinHttpQueryAuthSchemes

使用 401 或 407 状态代码接收响应时,可以使用 WinHttpQueryAuthSchemes 分析身份验证标头来确定支持的身份验证方案和身份验证目标。 身份验证目标是请求身份验证的服务器或代理。 WinHttpQueryAuthSchemes 还根据服务器建议的身份验证方案首选项,根据可用方案确定第一个身份验证方案。 选择身份验证方案的方法是 RFC 2616建议的行为。

WinHttpSetCredentials 使应用程序能够指定用于目标服务器或代理的有效用户名和密码的身份验证方案。 设置凭据并重新发送请求后,将自动生成必要的标头并将其添加到请求。 由于某些身份验证方案需要多个事务 WinHttpSendRequest 可能会返回错误,ERROR_WINHTTP_RESEND_REQUEST。 遇到此错误时,应用程序应继续重新发送请求,直到收到不包含 401 或 407 状态代码的响应。 200 状态代码指示资源可用且请求成功。 有关可返回的其他状态代码,请参阅 HTTP 状态代码

如果在将请求发送到服务器之前已知可接受的身份验证方案和凭据,则应用程序可以在 调用 winHttpSendRequest之前调用 WinHttpSetCredentials。 在这种情况下,WinHTTP 尝试通过向服务器提供凭据或 身份验证数据, 初始请求中向服务器提供身份验证数据。 预身份验证可以减少身份验证过程中的交换数,从而提高应用程序性能。

预身份验证可用于以下身份验证方案:

  • 基本 - 始终可能。
  • 谈判解决 Kerberos - 很可能:唯一的例外是客户端和域控制器之间的时间倾斜关闭。
  • (协商解决 NTLM) - 永远不可能。
  • NTLM - 仅在 Windows Server 2008 R2 中可能。
  • 摘要 - 永远不可能。
  • 护照 - 永远不可能;初始质询响应后,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,这允许默认凭据仅用于 Intranet 请求。 自动登录策略仅适用于 NTLM 和协商身份验证方案。 凭据永远不会与其他方案自动传输。

可以使用具有 WINHTTP_OPTION_AUTOLOGON_POLICY 标志的 WinHttpSetOption 函数来设置自动登录策略。 此标志仅适用于请求句柄。 将策略设置为WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW时,可以将默认凭据发送到所有服务器。 将策略设置为WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH时,不能使用默认凭据进行身份验证。 强烈建议在 MEDIUM 级别使用自动登录。

存储的用户名和密码

Windows XP 引入了存储用户名和密码的概念。 如果用户的 Passport 凭据通过 Passport 注册向导 或标准 凭据对话框保存,则会将其保存在存储的用户名和密码中。 在 Windows XP 或更高版本上使用 WinHTTP 时,如果未显式设置凭据,WinHTTP 会自动使用存储的用户名和密码中的凭据。 这类似于支持 NTLM/Kerberos 的默认登录凭据。 但是,使用默认 Passport 凭据不受自动登录策略设置的约束。