Если вы пропустили предыдущие три части, вы можете легко найти их здесь:
- Создание клиента Imgur для Windows Phone — часть 1 — Core & Main Gallery
- Создание клиента Imgur для Windows Phone — часть 2 — бесконечная прокрутка изображений
- Создание клиента Imgur для Windows Phone — часть 3 — просмотр сведений об изображении
Сегодня я собираюсь поговорить об аутентификации пользователя, потому что некоторые действия требуют входа пользователя в сервис. Imgur API использует поток аутентификации OAuth 2.0 (спецификация сервиса доступна
здесь ).
Чтобы разрешить пользователю доступ к странице аутентификации, вы можете добавить кнопку на панель приложения (она уже была там для вашего удобства в предыдущей выпадающей панели ).
Но кнопка должна куда-то идти, поэтому вам нужно создать новую страницу аутентификации, которая позволила бы пользователю вводить свои учетные данные и передавать их службе Imgur для проверки. Я добавил новый AuthPage.xaml, который не содержит ничего, кроме компонента WebBrowser .
<phone:PhoneApplicationPage x:Class="Imagine.AuthPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" mc:Ignorable="d" shell:SystemTray.IsVisible="False"> <!--LayoutRoot is the root grid where all page content is placed--> <Grid x:Name="LayoutRoot" Background="Transparent"> <phone:WebBrowser x:Name="authBrowser"></phone:WebBrowser> </Grid> </phone:PhoneApplicationPage>
При создании начального шага аутентификации нам нужно создать специальный URL-адрес, содержащий идентификатор клиента, а также секрет клиента, который бы идентифицировал приложение в качестве источника запроса. Я собираюсь перейти к этому конкретному URL-адресу в момент загрузки страницы, поскольку его единственная цель — получить учетные данные пользователя. Это можно сделать в обработчике событий AuthPage_Loaded :
void AuthPage_Loaded(object sender, RoutedEventArgs e) { authBrowser.Navigate(new Uri(string.Format("https://api.imgur.com/oauth2/authorize?client_id={0}&response_type=token", ConstantContainer.IMGUR_CLIENT_ID))); }
Помните, что внутри я храню идентификатор клиента и секрет в классе ConstantContainer . Как только страница загрузится, вы увидите приглашение ввести имя пользователя и пароль пользователя, который хочет пройти аутентификацию:
Вы можете задаться вопросом — почему я использую тип ответа токена вместо PIN или кода. Таким образом, я могу легко разобрать код из URL, не заставляя пользователя вводить PIN-код еще раз или выполнять дублирующие HTTP-запросы. На самом деле, когда дело доходит до выбора между кодом авторизации или токеном, это сводится к личным предпочтениям, так как вам все равно придется обрабатывать обратное чтение URL.
Поскольку сам по себе обратный вызов мне не нужен, я могу перехватить URL-адрес при навигации через обработчик событий Navigating и получить токены доступа и обновления:
void authBrowser_Navigating(object sender, NavigatingEventArgs e) { if (e.Uri.ToString().Contains("access_token=")) { string uriString = e.Uri.ToString(); int indexOfSharp = uriString.IndexOf("#"); string query = uriString.Substring(indexOfSharp + 1, uriString.Length - indexOfSharp - 1); string accessToken = query.Split('&') .Where(s => s.Split('=')[0] == "access_token") .Select(s => s.Split('=')[1]) .FirstOrDefault(); string refreshToken = query.Split('&') .Where(s => s.Split('=')[0] == "refresh_token") .Select(s => s.Split('=')[1]) .FirstOrDefault(); string username = query.Split('&') .Where(s => s.Split('=')[0] == "account_username") .Select(s => s.Split('=')[1]) .FirstOrDefault(); } else { Debug.WriteLine(e.Uri.ToString()); } }
Если вы решите использовать код авторизации, не стесняйтесь использовать мой класс AuthHelper :
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Text; namespace Imagine.Core { public class AuthHelper { public static void AuthenticateCode(string code, Action<KeyValuePair<string,string>> onTokenResponseReceived) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://api.imgur.com/oauth2/token"); string data = string.Format("client_id={0}&client_secret={1}&grant_type=authorization_code&code={2}", ConstantContainer.IMGUR_CLIENT_ID, ConstantContainer.IMGUR_CLIENT_SECRET, code); byte[] binaryDataContent = Encoding.UTF8.GetBytes(data); request.ContentType = "application/x-www-form-urlencoded"; request.Method = "POST"; request.BeginGetRequestStream(new AsyncCallback((n) => { using (Stream stream = (Stream)request.EndGetRequestStream(n)) { stream.Write(binaryDataContent, 0, binaryDataContent.Length); } request.BeginGetResponse(new AsyncCallback((ia) => { HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(ia); using (StreamReader reader = new StreamReader(response.GetResponseStream())) { Debug.WriteLine(reader.ReadToEnd()); } }), null); }),null); } } }
На этом этапе я могу сделать еще один шаг и получить класс ImgurAuthUser , который сохранит токены и позволит мне легко сериализовать и десериализовать контент. Вот структура класса:
namespace Imagine.ImgurAPI { public class ImgurAuthUser { public string AccessToken { get; set; } public string RefreshToken { get; set; } public string Username { get; set; } } }
Изменяя обработчик Navigating, мы можем получить это:
void authBrowser_Navigating(object sender, NavigatingEventArgs e) { if (e.Uri.ToString().Contains("access_token=")) { string uriString = e.Uri.ToString(); int indexOfSharp = uriString.IndexOf("#"); string query = uriString.Substring(indexOfSharp + 1, uriString.Length - indexOfSharp - 1); ImgurAuthUser user = new ImgurAuthUser(); user.AccessToken = query.Split('&') .Where(s => s.Split('=')[0] == "access_token") .Select(s => s.Split('=')[1]) .FirstOrDefault(); user.RefreshToken = query.Split('&') .Where(s => s.Split('=')[0] == "refresh_token") .Select(s => s.Split('=')[1]) .FirstOrDefault(); user.Username = query.Split('&') .Where(s => s.Split('=')[0] == "account_username") .Select(s => s.Split('=')[1]) .FirstOrDefault(); } else { Debug.WriteLine(e.Uri.ToString()); } }
Отлично, теперь, когда у меня есть метаданные аутентификации, мне нужно их сохранить. Для этого я создал вспомогательную функцию SerializeAuthMetadata в классе AuthHelper :
/// <summary> /// Serializes the authentication metadata returned from the service request /// to local storage. /// </summary> /// <param name="user">The filled instance of the user metadata carrier class.</param> /// <returns>If successful, is true.</returns> public static bool SerializeAuthMetadata(ImgurAuthUser user) { try { IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication(); using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("auth.xml", FileMode.Create, file)) { XmlSerializer serializer = new XmlSerializer(typeof(ImgurAuthUser)); serializer.Serialize(stream, user); } return true; } catch { return false; } }
На этом этапе имеет смысл на самом деле вызывать эту функцию при сохранении данных:
if (AuthHelper.SerializeAuthMetadata(user)) { if (NavigationService.CanGoBack) NavigationService.GoBack(); } else { MessageBox.Show("Ooops! Couldn't store the authentication metadata. Make sure that you have enought free space on the phone.", "Imagine", MessageBoxButton.OK); }
Как только данные успешно сохранены, я просто возвращаюсь на главную страницу, где отображаются изображения из главной галереи. Но здесь есть проблема — я все еще вижу кнопку, которая используется для входа в систему. Есть несколько подходов, которые можно использовать здесь. Я могу рассматривать ее как универсальную кнопку, которая позволила бы мне получить доступ к странице информации об учетной записи, когда учетные данные уже доступны, или она могла бы направить пользователя на страницу аутентификации, когда ее нет. Или я мог бы иметь отдельные кнопки для информации об учетной записи и выходить из системы всякий раз, когда сохраняются учетные данные.
Я собираюсь перейти к первому сценарию и переназначить его на кнопку « учетная запись ».
Для обработчика событий Click я могу просто проверить, существует ли файл auth.xml — он будет удален, когда пользователь выйдет из системы.
private void ApplicationBarIconButton_Click_1(object sender, System.EventArgs e) { IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication(); if (!file.FileExists("auth.xml")) { NavigationService.Navigate(new Uri("/AuthPage.xaml", UriKind.Relative)); } else { // Placeholder for navigation to the account page. } }
Вот и все. Теперь вы можете выполнять аутентифицированные звонки. Как обычно, вы можете скачать последнюю версию исходного кода здесь .