WinHTTP의 인증

일부 HTTP 서버 및 프록시는 인터넷의 리소스에 대한 액세스를 허용하기 전에 인증이 필요합니다. WinHTTP(Microsoft Windows HTTP 서비스) 함수는 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에서 지원하는 인증 체계, 인증 유형 및 스키마에 대한 설명이 포함되어 있습니다.

구성표 형식 Description
기본(일반 텍스트) Basic 사용자 이름과 암호를 포함하는 base64로 인코딩된 문자열을 사용합니다.
다이제스트 챌린지 응답 nonce(서버 지정 데이터 문자열) 값을 사용하는 데 문제가 있습니다. 유효한 응답에는 사용자 이름, 암호, 지정된 nonce 값, HTTP 동사 및 요청된 URI(Uniform Resource Identifier)의 체크섬이 포함됩니다.
NTLM 챌린지 응답 ID를 증명하기 위해 사용자 자격 증명을 사용하여 인증 데이터를 변환해야 합니다. NTLM 인증이 올바르게 작동하려면 동일한 연결에서 여러 교환이 수행되어야 합니다. 따라서 중간 프록시가 연결 유지 연결을 지원하지 않는 경우 NTLM 인증을 사용할 수 없습니다. WinHttpSetOption이 keep-alive 의미 체계를 사용하지 않도록 설정하는 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(애플리케이션 프로그래밍 인터페이스)는 인증이 필요한 상황에서 인터넷 리소스에 액세스하는 데 사용되는 두 가지 함수인 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에서만 가능합니다.
  • 다이제스트 - 불가능합니다.
  • Passport - 절대 가능하지 않습니다. 초기 챌린지 응답 후 WinHTTP는 쿠키를 사용하여 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 자격 증명의 사용은 자동 로그온 정책 설정의 적용을 받지 않습니다.