WinHTTP 中的身份验证

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

关于 HTTP 身份验证

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

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

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

注意

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

 

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

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

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

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

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

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

Scheme 类型 说明
基本 (纯文本) Basic 使用包含用户名和密码的 base64 编码 字符串。
摘要 质询-响应 使用 nonce (服务器指定的数据字符串) 值的挑战。 有效响应包含用户名、密码、给定 nonce 值、 HTTP 谓词的校验和,以及请求的统一资源标识符 (URI) 。
NTLM 质询-响应 要求使用用户凭据转换 身份验证数据 ,以证明身份。 要使 NTLM 身份验证正常工作,必须在同一连接上进行多次交换。 因此,如果干预代理不支持保持连接连接,则无法使用 NTLM 身份验证。 如果将 WinHttpSetOption 与禁用保持连接语义的 WINHTTP_DISABLE_KEEP_ALIVE 标志一起使用,NTLM 身份验证也会失败。
Passport 质询-响应 使用 Microsoft Passport 1.4
Negotiate 质询-响应 如果服务器和客户端都使用 Windows 2000 或更高版本,则使用 Kerberos 身份验证。 否则使用 NTLM 身份验证。 Kerberos 在 Windows 2000 及更高版本的操作系统中可用,被认为比 NTLM 身份验证更安全。 要使协商身份验证正常工作,必须在同一连接上进行多次交换。 因此,如果干预代理不支持保持连接连接,则无法使用协商身份验证。 如果将 WinHttpSetOption 与禁用 keep-alive 语义的 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 凭据不受自动登录策略设置的约束。