问题:
我正在尝试使外部身份验证与默认的 Web API 模板项目一起使用。我使用以下说明添加了对外部身份验证服务(FB/Google/Microsoft(的支持:http://www.asp.net/web-api/overview/security/external-authentication-services
仅供记录,我能够使外部身份验证与默认的SPA模板项目一起使用。此外,新的本地用户创建工作正常。
一旦我尝试使用我的客户端应用程序(基于 WPF(使用外部提供程序(例如 FB(注册用户,就会出现问题。
作为记录,我使用这两篇文章作为起点:http://leastprivilege.com/2013/11/26/dissecting-the-web-api-individual-accounts-templatepart-3-external-accounts/和线程#21065648在堆栈溢出。他们真的帮助我理解了整个逻辑。
以下是我已完成的步骤的简短概述:两个窗口,主窗口和一个用于外部身份验证提供程序的窗口,带有嵌入式 Web 浏览器流程:2.1. 用户打开应用,出现主窗口2.2. 用户单击按钮以获取所有支持的外部身份验证提供程序的列表,以下是代码中发生的情况:
var client = new HttpClient();
client.BaseAddress = new Uri(baseAddress);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage loginResponse = await client.GetAsync("api/Account/ExternalLogins?returnUrl=%2F&generateState=true");
if (loginResponse.IsSuccessStatusCode)
{
var externalLoginProviders = await loginResponse.Content.ReadAsAsync<IEnumerable<AuthenticationProvider>>();
// cleaning resources
client.Dispose();
loginResponse.Dispose();
// obtained data is sent to UI
这将导致外部身份验证提供程序列表,其中包含名称、URL 和状态。URL是相对的,用作重定向到实际提供商(例如FB或Google(。
2.3. 一旦填充了外部身份验证提供商的列表,用户就可以输入他/她的电子邮件,然后单击"登录"按钮,这将导致以下内容:
在第二个窗口中,使用嵌入式 Web 浏览器,后者将导航到上一步中获取的提供的 URL。如果用户成功登录到选定的提供商(例如Facebook(,并同意向我的应用程序(Facebook应用程序(提供必要的权限(个人资料(,则用户将导航回我们的网站,形式如下:
(our base url) /#access_token=XXX&token_type=bearer&expires=YYY
然后解析此地址,并以结构化形式保存地址参数(令牌等(以供以后使用。
之后,我立即转到 api/UserInfo 以了解用户是否已使用此外部身份验证提供程序登录:
var client = new HttpClient();
client.BaseAddress = new Uri(baseAddress);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// setting Bearer Token obtained from Auth provider
client.SetBearerToken(result.AccessToken);
// calling /api/Account/UserInfo
var userInfoResponse = await client.GetAsync("api/Account/UserInfo");
var userInfoMessage = userInfoResponse.Content.ReadAsStringAsync().Result;
var userInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(userInfoMessage);
// cleaning resources
client.Dispose();
userInfoResponse.Dispose();
if (userInfo.hasRegistered == true)
{
// going to login
}
在那之后,假设我们的用户没有根据他/她使用给定的外部身份验证提供程序(例如FB(的登录来创建用户,但是,我们执行以下操作:
var data = new Dictionary<string, string>();
data.Add("Email", this.externalUserEmailTextBox.Text);
var registerExternalUrl = new Uri(string.Concat(baseAddress, @"api/Account/RegisterExternal"));
var client = new HttpClient();
client.BaseAddress = new Uri(baseAddress);
// setting Bearer Token obtained from External Authentication provider
client.SetBearerToken(result.AccessToken);
var response = client.PostAsync(registerExternalUrl.ToString(), new FormUrlEncodedContent(data)).Result;
此时,应该创建用户(除了取消注释提供程序的应用程序 ID 和 Startup.Auth.cs 中的应用程序秘密行外,我在服务端没有任何更改(。
不幸的是,这根本不会发生。相反,我得到"内部服务器错误",这意味着这一行,
var info = await Authentication.GetExternalLoginInfoAsync();
带来空值,因此系统无法使用提供的持有者令牌正确进行身份验证。
我不明白为什么。框架中似乎有什么东西坏了,或者我做错了什么......
溶液:
多亏了@berhir,这是解决方案:
在 MainWindow.xaml.cs 中,定义 cookieContainer
CookieContainer cookieContainer;
在MainWindow_Loaded事件处理程序中,实例化它:
// we create new cookie container cookieContainer = new CookieContainer();
一旦用户要求应用显示所有外部登录提供程序,就可以使用以下建议的代码实例化 HttpClient,方法是@berhir:
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer }) using (var client = new HttpClient(handler) { BaseAddress = baseAddress }) { // send request client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); HttpResponseMessage loginResponse = await client.GetAsync("api/Account/ExternalLogins?returnUrl=%2F&generateState=true");
一旦获得外部身份验证提供程序的列表,下一步就是显示第二个窗口,其中包含嵌入式 WebBrowser。在那里,您必须为 cookie 声明两个 WinAPI 调用:
[DllImport("wininet.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern bool InternetSetCookie(string lpszUrlName, string lbszCookieName, string lpszCookieData); [DllImport("wininet.dll", SetLastError = true)] public static extern bool InternetGetCookieEx(string url, string cookieName, StringBuilder cookieData, ref int size, Int32 dwFlags, IntPtr lpReserved); // and private const Int32 InternetCookieHttponly = 0x2000; /// <summary> /// Gets the URI cookie container. /// </summary> /// <param name="uri">The URI.</param> /// <returns></returns> public static CookieContainer GetUriCookieContainer(Uri uri) { CookieContainer cookies = null; // Determine the size of the cookie int datasize = 8192 * 16; StringBuilder cookieData = new StringBuilder(datasize); if (!InternetGetCookieEx(uri.ToString(), null, cookieData, ref datasize, InternetCookieHttponly, IntPtr.Zero)) { if (datasize < 0) return null; // Allocate stringbuilder large enough to hold the cookie cookieData = new StringBuilder(datasize); if (!InternetGetCookieEx(uri.ToString(), null, cookieData, ref datasize, InternetCookieHttponly, IntPtr.Zero)) return null; } if (cookieData.Length > 0) { cookies = new CookieContainer(); cookies.SetCookies(uri, cookieData.ToString().Replace(';', ',')); } return cookies; }
此带有嵌入式浏览器的窗口被实例化,构造函数接受多个参数,包括起始 URL(到外部身份验证提供程序(、结束 URL(以"#access_token=..."或"error...",回调,更重要的是,使用原始cookieContainer。我们使用InternetSetCookie WinAPI方法将原始cookieContainer传递给WebBrowser的会话:
// set cookies var cookies = cookieContainer.GetCookies(baseAddress).OfType<Cookie>().ToList(); foreach (var cookie in cookies) { InternetSetCookie(startUrl, cookie.Name, cookie.Value); }
因此,一旦用户成功登录到选定的外部身份验证提供程序(例如Facebook(,使用InternetGetCookieEx WinAPI调用获得的更新cookieContainer(包括在第一个HttpClient调用中设置的cookie,以及在登录到外部身份验证提供程序后立即在WebBrowser中设置的cookie(将通过回调发送回MainWindow.xaml.cs:
if (this.callback != null) { var cookies = GetUriCookieContainer(e.Uri); this.callback(new AuthResult(e.Uri, this.providerName, cookies)); }
在那里,我们向 api/Account/UserInformation 和 api/Account/RegisterExternal 发出两个新请求:
this.cookieContainer = result.CookieContainer; // where result is AuthResult containing the cookies obtained from WebBrowser's session using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer }) using (var client = new HttpClient(handler) { BaseAddress = baseAddress }) { // send request client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // setting Bearer Token obtained from Auth provider client.SetBearerToken(result.AccessToken); // calling /api/Account/UserInfo var userInfoResponse = await client.GetAsync("api/Account/UserInfo"); var userInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(userInfoMessage); if (userInfo.hasRegistered == false) { var data = new Dictionary<string, string>(); data.Add("Email", this.externalUserEmailTextBox.Text); var registerExternalUrl = new Uri(string.Concat(baseAddress, @"api/Account/RegisterExternal")); var content = new FormUrlEncodedContent(data); var response = client.PostAsync(registerExternalUrl.ToString(), content).Result; // obtaining content var responseContent = response.Content.ReadAsStringAsync().Result; if (response != null && response.IsSuccessStatusCode) { MessageBox.Show("New user registered, with " + result.ProviderName + " account"); }
所以,我们开始了。Cookie 在整个生命周期内使用,从第一个 HttpClient 请求到我们使用 api/account/registerExternal 注册新用户的最后一刻。
看来你忘了处理饼干。最简单的方法是对所有请求使用相同的 HttpClient 实例,或者您可以使用同一个 CookieContainer。
var cookieContainer = new CookieContainer();
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
{
// send request
}