EternalWindows
WinHTTP / 認証の設定

ローカル上に存在するhtmlファイルをweb上にアップした場合、 それは自分以外のユーザーでもアクセスできるwebページへと変化します。 webというグローバルな領域にアップした以上、 このような不特定多数のアクセスは避けられないものですが、 そのアクセスしたページを実際に閲覧できるかどうかは、また別の話になります。 これは、サーバー上で認証を行うための設定をしておけば、 ユーザー名とパスワードを知り得るユーザーのみしか閲覧できなくなるからです。 たとえば、次に示すURLをブラウザでアクセスすると、下図のような認証を要求するダイアログが表示されます。

http://eternalwindows.jp/network/winhttp/sec/sample.html

上記URLのファイルが存在しているフォルダでは、Basic認証をするための設定が行われています。 この認証はクライアントにユーザー名とパスワードを要求するため、 ブラウザはダイアログを通じてユーザーからこれを取得し、サーバーへ送信します。 そして認証が成功した場合は、実際にページを閲覧できるようになります。 上記URLの認証を成功させるには、ユーザー名としてuser、パスワードとしてpassを入力します。

認証を必要とするURLにアクセスする場合は、 その認証に必要となるユーザー名とパスワードを、予めWinHTTPに知らせておく必要があります。 これは、WinHttpSetCredentialsで行います。

BOOL WINAPI WinHttpSetCredentials(
  HINTERNET hRequest,
  DWORD AuthTargets,
  DWORD AuthScheme,
  LPCWSTR pwszUserName,
  LPCWSTR pwszPassword,
  LPVOID pAuthParams
);

hRequestは、リクエストハンドルを指定します。 AuthTargetsは、通常WINHTTP_AUTH_TARGET_SERVERを指定します。 AuthSchemeは、認証スキームを指定します。 WINHTTP_AUTH_SCHEME_BASICを指定すると、Basic認証を使用することになります。 pwszUserNameは、認証に使用するユーザー名を指定します。 pwszPasswordは、認証に使用するパスワードを指定します。 pAuthParamsは、予約されているためNULLを指定します。

今回のプログラムは、Basic認証が必要なページにアクセスします。

#include <windows.h>
#include <winhttp.h>

#pragma comment (lib, "winhttp.lib")

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HINTERNET      hSession, hConnect, hRequest;
	URL_COMPONENTS urlComponents;
	WCHAR          szHostName[256], szUrlPath[2048];
	WCHAR          szUrl[] = L"http://eternalwindows.jp/network/winhttp/sec/sample.html";
	DWORD          dwSize;
	DWORD          dwStatusCode;
	BYTE           buffer[4096];

	hSession = WinHttpOpen(L"Sample Application/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
	if (hSession == NULL)
		return 0;

	ZeroMemory(&urlComponents, sizeof(URL_COMPONENTS));
	urlComponents.dwStructSize     = sizeof(URL_COMPONENTS);
	urlComponents.lpszHostName     = szHostName;
	urlComponents.dwHostNameLength = sizeof(szHostName) / sizeof(WCHAR);
	urlComponents.lpszUrlPath      = szUrlPath;
	urlComponents.dwUrlPathLength  = sizeof(szUrlPath) / sizeof(WCHAR);

	if (!WinHttpCrackUrl(szUrl, lstrlenW(szUrl), 0, &urlComponents)) {
		WinHttpCloseHandle(hSession);
		return 0;
	}
	
	hConnect = WinHttpConnect(hSession, szHostName, INTERNET_DEFAULT_PORT, 0);
	if (hConnect == NULL) {
		WinHttpCloseHandle(hSession);
		return 0;
	}

	hRequest = WinHttpOpenRequest(hConnect, L"GET", szUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
	if (hRequest == NULL) {
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);
		return 0;
	}
	
	WinHttpSetCredentials(hRequest, WINHTTP_AUTH_TARGET_SERVER, WINHTTP_AUTH_SCHEME_BASIC, L"user", L"pass", NULL);

	if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) {
		WinHttpCloseHandle(hRequest);
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);
		return 0;
	}

	WinHttpReceiveResponse(hRequest, NULL);

	dwSize = sizeof(DWORD);
	WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &dwStatusCode, &dwSize, WINHTTP_NO_HEADER_INDEX);
	if (dwStatusCode == HTTP_STATUS_OK) {
		WinHttpReadData(hRequest, buffer, sizeof(buffer), NULL);
		MessageBoxA(NULL, (LPSTR)buffer, "ボディ", MB_OK);
	}
	else {
		TCHAR szBuf[256];
		wsprintf(szBuf, TEXT("Status Code %d"), dwStatusCode);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
	}

	WinHttpCloseHandle(hRequest);
	WinHttpCloseHandle(hConnect);
	WinHttpCloseHandle(hSession);
	
	return 0;
}

WinHttpSendRequestでHTTPリクエストを送信する前に、WinHttpSetCredentialsで認証情報を設定します。 指定したユーザー名とパスワードが正しい場合は、dwStatusCodeがHTTP_STATUS_OKになりますが、 正しくない場合はHTTP_STATUS_DENIED(401)が返ります。

認証の判定

ブラウザのようなアプリケーションでは、アクセスすることになるURLが動的に決定されるため、 今回のように認証情報を静的に設定する方法は使用できません。 実行時において、そのURLが認証を必要としていることを理解し、 ユーザーに対してユーザー名をパスワードを要求する処理が必要になります。 このような例を次に示します。

#include <windows.h>
#include <winhttp.h>
#include <wincred.h>

#pragma comment (lib, "winhttp.lib")
#pragma comment (lib, "credui.lib")

BOOL SendRequest(HINTERNET hRequest, LPWSTR lpszHostName);
DWORD GetStatusCode(HINTERNET hRequest);
BOOL ShowCredentialDialog(LPWSTR lpszHostName, LPWSTR lpszUserName, DWORD dwUserNameLen, LPWSTR lpszPassword, DWORD dwPasswordLen);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HINTERNET      hSession, hConnect, hRequest;
	URL_COMPONENTS urlComponents;
	WCHAR          szHostName[256], szUrlPath[2048];
	WCHAR          szUrl[] = L"任意のURL";
	BYTE           buffer[4096];
	BOOL           bSSL;

	hSession = WinHttpOpen(L"Sample Application/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
	if (hSession == NULL)
		return 0;

	ZeroMemory(&urlComponents, sizeof(URL_COMPONENTS));
	urlComponents.dwStructSize     = sizeof(URL_COMPONENTS);
	urlComponents.lpszHostName     = szHostName;
	urlComponents.dwHostNameLength = sizeof(szHostName) / sizeof(WCHAR);
	urlComponents.lpszUrlPath      = szUrlPath;
	urlComponents.dwUrlPathLength  = sizeof(szUrlPath) / sizeof(WCHAR);

	if (!WinHttpCrackUrl(szUrl, lstrlenW(szUrl), 0, &urlComponents)) {
		WinHttpCloseHandle(hSession);
		return 0;
	}
	
	bSSL = urlComponents.nScheme == INTERNET_SCHEME_HTTPS;

	hConnect = WinHttpConnect(hSession, szHostName, INTERNET_DEFAULT_PORT, 0);
	if (hConnect == NULL) {
		WinHttpCloseHandle(hSession);
		return 0;
	}

	hRequest = WinHttpOpenRequest(hConnect, L"GET", szUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, bSSL ? WINHTTP_FLAG_SECURE : 0);
	if (hRequest == NULL) {
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);
		return 0;
	}

	if (SendRequest(hRequest, szHostName)) {
		WinHttpReadData(hRequest, buffer, sizeof(buffer), NULL);
		MessageBoxA(NULL, (LPSTR)buffer, "ボディ", MB_OK);
	}
	else {
		TCHAR szBuf[256];
		wsprintf(szBuf, TEXT("Status Code %d"), GetStatusCode(hRequest));
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
	}

	WinHttpCloseHandle(hRequest);
	WinHttpCloseHandle(hConnect);
	WinHttpCloseHandle(hSession);
	
	return 0;
}

BOOL SendRequest(HINTERNET hRequest, LPWSTR lpszHostName)
{
	WCHAR szUserName[256] = {0};
	WCHAR szPassword[256] = {0};
	DWORD dwStatusCode;
	DWORD dwAuthScheme;
	DWORD dwSupportedSchemes, dwFirstScheme, dwAuthTarget;

	WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
	WinHttpReceiveResponse(hRequest, NULL);

	dwStatusCode = GetStatusCode(hRequest);
	if (dwStatusCode == HTTP_STATUS_OK)
		return TRUE;

	if (dwStatusCode != HTTP_STATUS_DENIED)
		return FALSE;
	
	WinHttpQueryAuthSchemes(hRequest, &dwSupportedSchemes, &dwFirstScheme, &dwAuthTarget);
	if (dwSupportedSchemes & WINHTTP_AUTH_SCHEME_NEGOTIATE)
		dwAuthScheme = WINHTTP_AUTH_SCHEME_NEGOTIATE;
	else if (dwSupportedSchemes & WINHTTP_AUTH_SCHEME_NTLM)
		dwAuthScheme = WINHTTP_AUTH_SCHEME_NTLM;
	else if (dwSupportedSchemes & WINHTTP_AUTH_SCHEME_PASSPORT)
		dwAuthScheme = WINHTTP_AUTH_SCHEME_PASSPORT;
	else if (dwSupportedSchemes & WINHTTP_AUTH_SCHEME_DIGEST)
		dwAuthScheme = WINHTTP_AUTH_SCHEME_DIGEST;
	else if (dwSupportedSchemes & WINHTTP_AUTH_SCHEME_BASIC)
		dwAuthScheme = WINHTTP_AUTH_SCHEME_BASIC;
	else
		return FALSE;

	if (!ShowCredentialDialog(lpszHostName, szUserName, sizeof(szUserName) / sizeof(WCHAR), szPassword, sizeof(szPassword) / sizeof(WCHAR)))
		return FALSE;

	WinHttpSetCredentials(hRequest, WINHTTP_AUTH_TARGET_SERVER, dwAuthScheme, szUserName, szPassword, NULL);
	
	WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
	WinHttpReceiveResponse(hRequest, NULL);

	return GetStatusCode(hRequest) == HTTP_STATUS_OK;
}

DWORD GetStatusCode(HINTERNET hRequest)
{
	DWORD dwSize;
	DWORD dwStatusCode;

	dwSize = sizeof(DWORD);
	WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &dwStatusCode, &dwSize, WINHTTP_NO_HEADER_INDEX);
	
	return dwStatusCode;
}

BOOL ShowCredentialDialog(LPWSTR lpszHostName, LPWSTR lpszUserName, DWORD dwUserNameLen, LPWSTR lpszPassword, DWORD dwPasswordLen)
{
	DWORD dwResult;
	WCHAR szUpn[256] = {0};
	WCHAR szDomain[256] = {0};

	dwResult = CredUIPromptForCredentialsW(NULL, lpszHostName, NULL, 0, szUpn, 256, lpszPassword, dwPasswordLen, NULL, CREDUI_FLAGS_DO_NOT_PERSIST);
	if (dwResult != NO_ERROR)
		return FALSE;

	CredUIParseUserNameW(szUpn, lpszUserName, dwUserNameLen, szDomain, 256);

	return TRUE;
}

アクセスするURLが予め決まっていない場合は、URLがhttpsから始まることも考えられます。 この場合はSSL通信を行うべきであるため、WinHttpOpenRequestの最終引数にWINHTTP_FLAG_SECUREを指定します。 SendRequestはHTTPリクエストを発行する自作関数であり、 スタータスコードとしてHTTP_STATUS_OKが返った場合は処理を成功とみなします。 それ以外の値が返った場合は処理と失敗とみなしますが、 HTTP_STATUS_DENIEDについては例外です。 この場合は認証が失敗ということであるため、どのような認証が求められているかを理解し、 認証の設定を行ってから、再度リクエストを発行することになります。 WinHttpQueryAuthSchemesを呼び出せば、求められている認証をdwSupportedSchemesで取得できますが、 これには複数の認証スキームが含まれていることがあります。 この場合どれを使用すればよいかというと、条件式に記述されているように、 WINHTTP_AUTH_SCHEME_NEGOTIATEから優先して確認していくのが妥当であると思われます。 何故、WINHTTP_AUTH_SCHEME_BASICが最も優先されないのかというと、 この認証がユーザー名とパスワードを暗号化せずに送信することになっているからです。 このことを踏まえた場合、dwSupportedSchemes & WINHTTP_AUTH_SCHEME_BASICの条件式は、 下記のように慎重に扱うべきといえるかもしれません。

else if (dwSupportedSchemes & WINHTTP_AUTH_SCHEME_BASIC) {
	DWORD dwFlag;
	DWORD dwSize = sizeof(dwFlag);

	WinHttpQueryOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &dwFlag, &dwSize);
	if (dwFlag & SECURITY_FLAG_SECURE)
		dwAuthScheme = WINHTTP_AUTH_SCHEME_BASIC;
	else
		return FALSE;
}

WinHttpQueryOptionにWINHTTP_OPTION_SECURITY_FLAGSを指定してSECURITY_FLAG_SECUREが返った場合、 現在SSL通信が行われていることを意味します。 この場合は、通信全体が暗号化されることになるため、当然ユーザー名とパスワードも暗号化されることになり、 問題なくBasic認証を行うことができます。 しかし、SSL通信が行われてない場合はユーザー名とパスワードが暗号化されないことになりますから、 この時点で失敗とみなすようにしています。

認証スキームを取得したら、ShowCredentialDialogという自作関数でユーザー名とパスワードの入力を促します。 認証のダイアログはCredUIPromptForCredentialsで表示できますが、 この関数はユーザー名を"ドメイン\ユーザー名"という形で返すため、 ユーザー名のみを取り出すためにCredUIParseUserNameも呼び出しています。 ユーザー名とパスワードを取得したらWinHttpSetCredentialsで認証の設定を行い、 WinHttpSendRequestを呼び出すことになります。



戻る