Статьи

Создание клиента Imgur для Windows Phone — Часть 4 — Аутентификация


Если вы пропустили предыдущие три части, вы можете легко найти их здесь:

Сегодня я собираюсь поговорить об аутентификации пользователя, потому что некоторые действия требуют входа пользователя в сервис.
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.
    }
}

Вот и все. Теперь вы можете выполнять аутентифицированные звонки. Как обычно, вы можете  скачать последнюю версию исходного кода здесь .