【问题标题】:How to reset WinHTTP proxy credentials after a failed request?请求失败后如何重置 WinHTTP 代理凭据?
【发布时间】:2014-10-02 15:24:05
【问题描述】:

我需要编写代码来下载文件,要求如下。如果应用程序配置为使用代理,请尝试通过代理下载。如果失败,请尝试直接连接。如果未配置代理,请尝试直接连接。高级伪代码:

if(ProxyEnabled)
    if(!DownloadWithProxy())
        DownloadWithoutProxy()
else
    DownloadWithoutProxy()

我正在使用 WinHTTP,因为此代码将在服务中运行。实际下载很简单,但代理设置有问题。我目前的伪代码如下:

hSession = WinHttpOpen(..., WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, ...)
hConnect = WinHttpConnect(hSession, ...)
hRequest = WinHttpOpenRequest(hConnect, ...)
if(Proxy.Enabled)
{
    // set proxy server
    WinHttpSetOption(hRequest, WINHTTP_OPTION_PROXY, ServerName)
    // set proxy credentials
    WinHttpSetCredentials(hRequest, WINHTTP_AUTH_TARGET_PROXY, UserName, Password)
    if(!DownloadFile(hRequest))
    {
        // reset proxy server
        WinHttpSetOption(hRequest, WINHTTP_OPTION_PROXY, NULL)
        // reset proxy credentials
        WinHttpSetCredentials(hRequest, WINHTTP_AUTH_TARGET_PROXY, NULL, NULL)
        DownloadFile(hRequest)
    }
}
else
{
    DownloadFile(hRequest)
}

DownloadFile() 在哪里执行标准的 WinHttpSendRequest()WinHttpReceiveResponse() 序列。一切正常,除非通过代理下载失败。发生这种情况时,调用WinHttpSetCredentials() 重置凭据失败(带有ERROR_INVALID_PARAMETER),因此第二次调用DownloadFile() 仍然尝试使用代理(即使我重置它)和凭据。注意:在这种特定情况下,我使用的是有效的代理服务器但无效的代理凭据来触发故障。

所以我想我的问题是最好的方法是什么?据我所知,无法通过WinHttpSetCredentials() 重置凭据集,所以我想我应该为每次调用DownloadFile() 重新创建请求,而不是通过重用单个请求对象来尝试“聪明” .

【问题讨论】:

    标签: windows winapi winhttp


    【解决方案1】:

    WinHttpSetCredentials() 有 6 个参数,其中一个是认证方案。您只显示了 4 个参数,并没有说明您使用的是哪种身份验证方案。总体而言,您的身份验证顺序与 MSDN 建议的不同:

    Authentication in WinHTTP

    典型的 WinHTTP 应用程序会完成以下步骤来处理身份验证。

    •使用 WinHttpOpenRequest 和 WinHttpSendRequest 请求资源。
    •使用 WinHttpQueryHeaders 检查响应标头。
    • 如果返回 401 或 407 状态码表明需要进行身份验证,请调用 WinHttpQueryAuthSchemes 以查找可接受的方案。
    •使用 WinHttpSetCredentials 设置身份验证方案、用户名和密码。
    •通过调用WinHttpSendRequest重新发送具有相同请求句柄的请求。

    还要注意这种情况:

    WinHttpSetCredentials 设置的凭据仅用于一个请求。 WinHTTP 不会缓存凭据以用于其他请求,这意味着必须编写可以响应多个请求的应用程序。如果重新使用经过身份验证的连接,其他请求可能不会受到挑战,但您的代码应该能够随时响应请求。

    上面链接的文档包含以下代码示例,说明如何正确使用WinHTTPSetCredentials()

    #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 );
    }
    

    您只需在适当的地方注入您的WinHttpSetOption(WINHTTP_OPTION_PROXY),并确保您正在处理代理和非代理连接的身份验证请求(以防目标 HTTP 服务器需要自己的身份验证)。

    【讨论】:

    • 我不确定这是否记录在任何地方,但我观察到的行为(通过 Wireshark)是,如果您在第一次调用 WinHttpSetCredentials() 之前调用 WinHttpSetCredentials(),那么 407 状态代码是透明地处理(显然假设有效凭据),因此无需显式处理(至少对于我的场景)。无论如何,一旦设置了凭据,我就无法弄清楚如何清除它们,因此我放弃了并采用了创建新请求对象的不太“聪明”的方法。我会接受你做出努力的回答。
    • 我的答案链接到的同一“WinHTTP 中的身份验证”页面记录了这一点:“如果在将请求发送到服务器之前已知可接受的身份验证方案和凭据,则应用程序可以调用WinHttpSetCredentials 在调用 WinHttpSendRequest 之前。在这种情况下,WinHTTP 通过在向服务器的初始请求中提供凭据或身份验证数据来尝试与服务器进行预身份验证。预身份验证可以减少身份验证过程中的交换次数,从而提高应用程序性能。 "
    猜你喜欢
    • 1970-01-01
    • 2016-03-05
    • 2013-10-18
    • 1970-01-01
    • 2022-01-06
    • 1970-01-01
    • 2021-09-17
    • 2018-10-22
    • 1970-01-01
    相关资源
    最近更新 更多