我正在开发需要Internet访问的WPF桌面应用程序。某些网络环境需要代理身份验证。使用this MSDN blog entry,我设法实现了捕获代理返回的407错误,并使用DomainCredentials
作为后备。但是,我无法实现博客文章中使用的CredentialPicker
,因为它来自我无法本地引用的UWP库()。我必须提供某种对话框供用户输入他/她的代理身份验证凭据,以防它们与DefaultCredentials
不同(尽管我不确定,这种极端情况多久出现一次,是否值得努力)。
我宁愿不必自己实现此功能,因为将来它可能必须支持智能卡或某些其他身份验证方法,而不仅仅是用户名/密码组合。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Security.Credentials.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238
namespace ProxyAuthentication
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
// global http client to make requests;
HttpClient appHttpClient;
// global credential cache for proxy credentials;
CredentialCache proxyCredCache;
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached. The Parameter
/// property is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
}
private async void BtnRequest_Click(object sender, RoutedEventArgs e)
{
//hard coded for my testing!
string result = await DoRequestAsync("http://bing.com");
//TODO: possibly log the result string for troubleshooting
}
private async Task<HttpResponseMessage> TryDomainCredentialsAsync(string theUri)
{
//set the proxy credentials to the Currently Logged on user credentials
WebRequest.DefaultWebProxy.Credentials = CredentialCache.DefaultCredentials;
return await appHttpClient.GetAsync(theUri);
}
private async Task<HttpResponseMessage> TryCachedCredentialsAsync(string theUri)
{
// try the global credential cache I created earlier!
WebRequest.DefaultWebProxy.Credentials = proxyCredCache;
return await appHttpClient.GetAsync(theUri);
}
// This will pop up a credenital dialog. It could use smart cards if that is supported by the proxy (they will just show up here)
private async Task<HttpResponseMessage> TryCredentialPickerAsync(string theUri, AuthenticationHeaderValue header)
{
// Basic and Digest credentials are easily decoded.
// You may want to test header.Scheme and see if it is Digest or Basic
// and warn the end user that they are about to supply their credentials over clear text
var proxy = WebRequest.DefaultWebProxy.GetProxy(new Uri(theUri));
CredentialPickerOptions credPickerOptions = new CredentialPickerOptions();
credPickerOptions.TargetName = WebRequest.DefaultWebProxy.GetProxy(new Uri(theUri)).DnsSafeHost;
credPickerOptions.Message = "Proxy Authentication required for: " + credPickerOptions.TargetName;
credPickerOptions.Caption = "Please enter your Proxy credentials";
credPickerOptions.CallerSavesCredential = false;
credPickerOptions.CredentialSaveOption = CredentialSaveOption.Hidden;
credPickerOptions.AuthenticationProtocol = GetAuthEnum(header.Scheme);
CredentialPickerResults credPickerResults = await CredentialPicker.PickAsync(credPickerOptions);
if (proxyCredCache == null)
{
proxyCredCache = new CredentialCache();
}
// see if credentials already exist and remove if they do!
var existingCred = proxyCredCache.GetCredential(new Uri(proxy.AbsoluteUri), credPickerOptions.AuthenticationProtocol.ToString());
if (existingCred != null)
{
proxyCredCache.Remove(new Uri(proxy.AbsoluteUri), credPickerOptions.AuthenticationProtocol.ToString());
existingCred = null;
}
// add the credentials entered to the cache
proxyCredCache.Add(new Uri(proxy.AbsoluteUri), credPickerOptions.AuthenticationProtocol.ToString(), new NetworkCredential(credPickerResults.CredentialUserName, credPickerResults.CredentialPassword));
// try the global credential cache I created!
WebRequest.DefaultWebProxy.Credentials = proxyCredCache;
return await appHttpClient.GetAsync(theUri);
}
//Helper function to translate a string to the authorization enum we need.
//There are other protocols but I could not test them so I will not include them.
private AuthenticationProtocol GetAuthEnum(string theVal)
{
AuthenticationProtocol returnVal = AuthenticationProtocol.Basic;
switch (theVal.ToLower())
{
case "basic":
{
returnVal = AuthenticationProtocol.Basic;
break;
};
case "digest":
{
returnVal = AuthenticationProtocol.Digest;
break;
};
case "negotiate":
{
returnVal = AuthenticationProtocol.Negotiate;
break;
};
case "ntlm":
{
returnVal = AuthenticationProtocol.Ntlm;
break;
};
case "kerberos":
{
returnVal = AuthenticationProtocol.Kerberos;
break;
};
};
return returnVal;
}
// simple helper to compare security of protocols
private bool newMoreSecure(AuthenticationHeaderValue currVal, AuthenticationHeaderValue newVal)
{
return GetAuthEnum(newVal.Scheme) > GetAuthEnum(currVal.Scheme);
}
// from the proxy auth headers, determing the most secure authentication and return it.
private AuthenticationHeaderValue ProxyAuthType(HttpHeaderValueCollection<AuthenticationHeaderValue> headers)
{
AuthenticationHeaderValue currHeader = null;
foreach (AuthenticationHeaderValue header in headers)
{
if (currHeader == null)
{
currHeader = header;
}
else
{
if (newMoreSecure(currHeader, header))
{
currHeader = header;
}
}
}
return currHeader;
}
// Kick off a simple Get request, accomodate proxy authentication
private async Task<string> DoRequestAsync(string theUri)
{
HttpResponseMessage theResponse = null;
string returnTxt = "";
if (appHttpClient == null)
{
appHttpClient = new HttpClient();
}
try
{
theResponse = await appHttpClient.GetAsync(theUri);
bool retry = true;
bool triedDomainCreds = false;
bool triedCachedCreds = false;
int retryCount = 3;
// if 407 was returned so we need to do some proxy authentication
while ((theResponse.StatusCode == HttpStatusCode.ProxyAuthenticationRequired) && (retry == true))
{
// find out what type of auth would be the most secure:
AuthenticationHeaderValue header = ProxyAuthType(theResponse.Headers.ProxyAuthenticate);
if (header != null)
{
// if possible to silently use domain credentials then try that first but only try once!
// We will loop back and use the credenial picker the next time if necessary
// Note:
// Basic and Digest credentials are easily decoded.
// You may want to test header.Scheme and see if it is Digest or Basic
// and warn the end user that they are about to supply their credentials over clear text.
// In this case I decided NEVER to allow the default credentials to be passed if Digest or Basic auth is used
// If the HTTP proxy requests BASIC auth because it is setup that way by the admin, there is not much we can do about it
// but I choose in my case not to silently use the default credentials with these less secure schemes
if ((triedDomainCreds == false) && (GetAuthEnum(header.Scheme) > AuthenticationProtocol.Digest))
{
try
{
theResponse = await TryDomainCredentialsAsync(theUri);
}
catch(Exception domainTryExeption)
{
// TODO: log this exception
returnTxt = domainTryExeption.Message;
// edge case. If you don't have Enterprise Authentication in your manifest
// (store policy may not allow your app to have this)
// you cannot use the current user credentials, so prompt for them instead
}
triedDomainCreds = true;
}
else
{
// see if we have credentials stored for this.
if ((triedCachedCreds == false) && (proxyCredCache != null))
{
theResponse = await TryCachedCredentialsAsync(theUri); // try them
triedCachedCreds = true;
}
else // last resort, try the credential picker!
{
theResponse = await TryCredentialPickerAsync(theUri, header);
retryCount--; // only try to get the creds retryCount times, then bail
// TODO: You probably want to delete the credential cache entry you just tried since if it did not work
}
}
// did we authorize through the proxy?
if (theResponse.StatusCode == HttpStatusCode.ProxyAuthenticationRequired)
{
// should we retry?
if (retryCount == 0)
{
retry = false;
}
}
else
{
// all other statuses return stop proxy auth
retry = false;
}
}
else
{
retry = false;
returnTxt = "Problem, expected and Authorization header and did not find one!";
}
}
}
catch (Exception theEx)
{
// TODO: some other problem so report it possibly to help the customer or log it for your own debugging!
returnTxt = theEx.Message;
}
if (theResponse != null)
{
returnTxt = theResponse.StatusCode.ToString();
}
TxtOut.Text = returnTxt;
return returnTxt;
}
}
}