Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Применение C++ REST SDK в приложениях Windows Store
Исходный код можно скачать по ссылке.
Продукты и технологии:
C++ REST SDK, OAuth
В статье рассматриваются:
- предыдущий пример Win32-приложения;
- варианты интеграции с Windows Runtime;
- использование класса WebAuthenticationBroker;
- создание цепочки асинхронных веб-запросов.
В предыдущей статье (msdn.microsoft.com/magazine/dn342869) я познакомил вас с C++ REST SDK и тем, как его можно использовать в Win32/MFC-приложениях. В этой статье мы обсудим, как C++ REST SDK можно интегрировать в приложения Windows Store. Одной из моих исходных целей в применении C++ REST SDK и класса аутентификации на основе OAuth было максимально полное использование стандартного C++ и лишь при необходимости взаимодействие со специфичными для платформы API. Кратко напомню суть предыдущей статьи.
- Код в классе аутентификации на основе OAuth использует только стандартные C++-типы — без специфичных для Windows типов.
- Код для выдачи веб-запросов к REST-сервису Dropbox использует типы из C++ REST SDK.
- Единственный специфичный для платформы код — это функция, которая запускает Internet Explorer, выполняет аутентификацию приложения и получает одобрение от портала Dropbox.
Я поставил те же цели в своем приложении Windows Store для поддержки аутентификации и загрузки файла в Dropbox. Я стремился написать максимально больше портируемого кода на C++ и взаимодействовать с Windows Runtime (WinRT) только при необходимости.
Проблемы с Win32-решением
Одним из крупных недостатков в предыдущем Win32-приложении была необходимость запуска внешнего приложения для выполнения процесса авторизации на основе OAuth. Это означало, что я должен был запускать Internet Explorer (можно было бы запускать и любой другой браузер), входить в Dropbox по своим удостоверениям, а затем выполнять необходимый рабочий процесс. Он проиллюстрирован на рис. 1 и 2.
Рис. 1. Вход в Dropbox по моим удостоверениям до авторизации доступа приложения
Рис. 2. Успешная авторизация моего приложения на портале Dropbox
Как видите, запуск внешнего приложения и предложение пользователям пройти рабочий процесс через внешнее приложение уводит фокус ввода из окна моего приложения. Как разработчик я также лишен стандартного механизма, через который мое приложение могло бы уведомляться о завершении этого рабочего процесса. При концентрации на асинхронном программировании и использовании C++ REST SDK, предназначенного для поддержки программирования на основе асинхронных задач, вынужденный запуск внешнего приложения со всей очевидностью является для меня неприятной ситуацией. Я исследовал подходы с применением именованных каналов (named pipes), проецируемых в память файлов (memory-mapped files) и т. д., но все эти подходы требуют создания другого приложения для хостинга экземпляра элемента управления «веб-браузер» и последующей записи значения, свидетельствующего об успехе, обратно через именованный канал, общую память или проецируемый в память файл. В итоге я остановился на использовании браузера для выполнения этой задачи, так как не хотел писать другую программу, которая обертывала бы элемент управления «веб-браузер».
Одним из крупных недостатков в предыдущем Win32-приложении была необходимость запуска внешнего приложения для выполнения процесса авторизации на основе OAuth.
Интеграция с Windows Runtime
Начав проектировать свое приложение под Windows Runtime, я рассмотрел несколько вариантов. Я вкратце расскажу о них здесь, а потом мы подробно обсудим выбранный мной подход.
- Используем активацию протокола и даем системе запустить подходящий процесс для обработки этого протокола, вызвав функцию Windows::System::Launcher::LaunchUriAsync. Это означает, что для URI на основе HTTPS операционная система запустит браузер по умолчанию. Это аналогично запуску Internet Explorer из Win32-примера, но с «двойной комиссией»: мое приложение Windows Store станет фоновым процессом, браузер по умолчанию запустится в полноэкранном режиме, и в самом худшем случае мое приложение будет приостановлено на время выполнения пользователем рабочего процесса. Совершенно не годится!
- Интегрируем в приложение элемент управления WebView. Использование XAML-элемента WebView позволяет встроить всю навигацию по рабочему процессу в контекст моего приложения. Теоретически, я также могу получать уведомления о завершении процесса, слушая событие window.external.notify, генерируемое элементом управления WebView. Однако на практике это событие генерируется, только если веб-страница генерирует событие уведомления. В моем случае страница Dropbox, где выполняется процесс авторизации, такое событие не генерирует. Неприятная ситуация!
- Используем WebAuthenticationBroker в моем приложении. Продолжая копаться в Windows Runtime, я случайно наткнулся на класс WebAuthenticationBroker. Судя по всему, он мог бы помочь мне в выполнении процесса авторизации, и, действительно, я сумел создать всю необходимую функциональность на его основе. Прежде чем перейти к рассмотрению кода, позвольте мне пояснить некоторые детали, касающиеся WebAuthenticationBroker.
WebAuthenticationBroker
В мире подключенных приложений, чтобы получить согласие и одобрение пользователя, важно запрашивать его удостоверения через безопасный и доверяемый механизм. Никто не хочет быть разработчиком, чьи приложения допускают утечку удостоверений пользователя или оказываются уязвимыми к скрытым атакам с целью похищения информации о пользователе. Windows Runtime включает ряд API и необходимые технологии, позволяющие разработчику безопасно передавать удостоверения пользователя. WebAuthenticationBroker — одно из таких средств, которое дает возможность приложениям Windows Store использовать протоколы аутентификации и авторизации через Интернет, такие как OAuth и OpenID. Как же это работает в моем приложении-примере для Dropbox?
- Я выдаю начальный асинхронный запрос к Dropbox, который возвращает маркер и секрет для моего приложения. Этот начальный запрос передается через функцию oAuthLoginAsync.
- Как только функция oAuthLoginAsync возвращает управление, я конструирую в продолжении последовательности URI, где должен начаться процесс авторизации. В своем примере я определил начальный URI как строковую константу:
const std::wstring DropBoxAuthorizeURI =
L"https://www.dropbox.com/1/oauth/authorize?oauth_token=";
- Затем я формирую URI HTTP-запроса, дописывая маркер, возвращенный Dropbox.
- В качестве дополнительного шага я конструирую параметр с URI обратного вызова, обращаясь к функции WebAuthenticationBroker::GetCurrentApplicationCallbackUri. Заметьте, что я не использовал URI обратного вызова в своем настольном приложении, так как этот параметр не обязателен и я полагался на Internet Explorer в выполнении задачи авторизации.
- Теперь строка запроса готова, и я могу выдать запрос. Вместо использования класса http_client или интерфейса IHttpWebRequest2 из C++ REST SDK для вызовов веб-сервиса я вызываю функцию WebAuthenticationBroker::AuthenticateAsync.
- Функция WebAuthenticationBroker::AuthenticateAsync принимает два параметра: перечисление WebAuthenticationOptions и URI. Перегруженный экземпляр той же функции принимает перечисление WebAuthenticationOptions и два URI, по одному из которых начинается процесс аутентификации, а по другому — заканчивается.
- Я использую первую версию функции AuthenticateAsync и передаю значение None для перечисления WebAuthenticationOptions, а также URI, сформированный для моего веб-запроса.
- WebAuthenticationBroker размещается между моим приложением и системой. В точке, где я вызываю AuthenticateAsync, он создает системный модальный диалог, который является модальным для моего приложения.
- Брокер подключает окно веб-хоста к созданному им модальному диалоговому окну.
- Затем брокер выбирает выделенный процесс контейнера приложений, отделенный от контейнера, в котором выполняется мое приложение. Это также приводит к очистке любых сохраненных данных в моем приложении.
- Далее брокер начинает процесс аутентификации в этом только что выбранном контейнере приложения и переходит к URI, указанному функцией AuthenticateAsync.
- Когда пользователи взаимодействуют с веб-страницами, брокер проверяет каждый URL для указанного URI обратного вызова.
- Как только обнаруживается совпадение, веб-хост прекращает навигацию и посылает брокеру сигнал. Брокер убирает диалоговое окно, очищает любые сохраненные файлы cookie, созданные веб-хостом, из контейнера приложения и возвращает данные протокола обратно приложению.
В мире подключенных приложений, чтобы получить согласие и одобрение пользователя, важно запрашивать его удостоверения через безопасный и доверяемый механизм.
Рис. 3 иллюстрирует модальный диалог WebAuthenticationBroker в приложении-примере Dropbox после того, как веб-хост перешел на начальный URI. Поскольку Dropbox ожидает входа пользователей до появления страницы авторизации, веб-хост перенаправляет процесс навигации на страницу входа в Dropbox.
Рис. 3. Страница входа в Dropbox, показываемая в модальном диалоге
Как только пользователь вошел в Dropbox, веб-хост переходит к URI авторизации. Это отражено на рис. 4. Из рис. 3 и 4 понятно, что диалог размещается поверх UI моего приложения. UI также остается согласованным безотносительно приложения-источника, вызывающего метод WebAuthenticationBroker::AuthenticateAsync. Поскольку вся пользовательская среда сохраняет согласованность, пользователи могут предоставлять удостоверения, не беспокоясь о приложениях, обрабатывающих эту информацию, и о ее случайной утечке.
Рис. 4. Dropbox запрашивает согласие пользователя на авторизацию приложения
Одна важная вещь, о которой я не упомянул, — необходимость вызова функции WebAuthenticationBroker::AuthenticateAsync из UI-потока. Все веб-запросы в C++ REST SDK выдаются в фоновом потоке, а вывести UI из фонового потока нельзя. Поэтому я использую системный диспетчер и вызываю его функцию-член RunAsync для отображения модального UI (рис. 5).
Рис. 5. Использование системного диспетчера для отображения модального UI
auto action = m_dispatcher->RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
ref new Windows::UI::Core::DispatchedHandler([this]()
{
auto beginUri = ref new Uri(ref new String(m_authurl.c_str()));
task<WebAuthenticationResult^> authTask(WebAuthenticationBroker::
AuthenticateAsync(WebAuthenticationOptions::None, beginUri));
authTask.then([this](WebAuthenticationResult^ result)
{
String^ statusString;
switch(result->ResponseStatus)
{
case WebAuthenticationStatus::Success:
{
auto actionEnable = m_dispatcher->RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
ref new Windows::UI::Core::DispatchedHandler([this]()
{
UploadFileBtn->IsEnabled = true;
}));
}
}
});
}));
Как только пользователь вошел в Dropbox, веб-хост переходит к URI авторизации.
По окончании процесса авторизации я снова запускаю диспетчер, чтобы сделать доступной кнопку Upload File в основном UI. Эта кнопка остается недоступной, пока пользователи не аутентифицировали и не авторизовали мое приложение для доступа к Dropbox.
Создание цепочки асинхронных веб-запросов
Теперь легко свести все воедино. Во всех функциях, не взаимодействующих с Windows Runtime, я повторно использовал код из моего настольного приложения. Крупных изменений в коде нет, кроме одного: в функции UploadFileToDropboxAsync вместо C++ iostream используется WinRT-объект StorageFile.
При написании приложений Windows Store приходится учитывать некоторые ограничения, с которыми вы должны смириться. Одно из них — необходимость использования WinRT-объектов StorageFile вместо C++-потоков (streams) для чтения и записи данных в файлы. При разработке приложения Windows Store с применением C++ REST SDK все операции, связанные с файлами, ожидают передачи объекта StorageFile, а не C++-объекта потока данных. Внеся это небольшое изменение, я смог повторно использовать весь свой стандартный C++-код, поддерживающий код OAuth-авторизации и Dropbox, в приложении-примере для Windows Store.
Вот как выглядит соответствующий псевдокод (индивидуальные функции мы обсудим после этого псевдокода):
При щелчке кнопки SignIn
Вызов функции oAuthLoginAsync
Затем вызов WebAuthenticationBroker::AuthenticateAsync
Затем делаем доступной кнопку Upload File в UI
При щелчке кнопки Upload File
Вызов функции Windows::Storage::Pickers::FileOpenPicker::
PickSingleFileAsync
Затем вызов функции oAuthAcquireTokenAsync
Затем вызов функции UploadFileToDropboxAsync
В обработчике событий кнопки SignInBtnClicked, показанном на рис. 6, я сначала выполняю простую проверку параметров, чтобы удостовериться, что в параметрах ConsumerKey и ConsumerSecret не передаются пустые строки, отправляемые потом в Dropbox для аутентификации. Затем я получаю экземпляр объекта Dispatcher, сопоставленного с текущим потоком CoreWindow, и сохраняю его как переменную-член класса MainPage. Dispatcher отвечает за обработку оконных сообщений и диспетчеризацию событий для приложения. Далее я создаю экземпляр класса OnlineIdAuthenticator. Этот класс содержит вспомогательные функции, которые позволяют мне выводить модальное диалоговое окно приложения и выполнять защищенный рабочий процесс авторизации. Это избавляет от необходимости запуска экземпляра браузера и перевода фокуса ввода с приложения в браузер.
Рис. 6. Функция SignInBtnClicked
void MainPage::SignInBtnClicked(Platform::Object^ sender,
RoutedEventArgs^ e)
{
if ((ConsumerKey->Text == nullptr) ||
(ConsumerSecret->Text == nullptr))
{
using namespace Windows::UI::Popups;
auto msgDlg = ref new MessageDialog(
"Please check the input for the Consumer Key and/or Consumer Secret tokens");
msgDlg->ShowAsync();
}
m_dispatcher =
Windows::UI::Core::CoreWindow::GetForCurrentThread()->Dispatcher;
m_creds = std::make_shared<AppCredentials>();
m_authenticator = ref new OnlineIdAuthenticator();
consumerKey = ConsumerKey->Text->Data();
consumerSecret = ConsumerSecret->Text->Data();
ConsumerKey->Text = nullptr;
ConsumerSecret->Text = nullptr;
OAuthLoginAsync(m_creds).then([this]
{
m_authurl = DropBoxAuthorizeURI;
m_authurl +=
utility::conversions::to_string_t(this->m_creds->Token());
m_authurl += L"&oauth_callback=";
m_authurl += WebAuthenticationBroker::
GetCurrentApplicationCallbackUri()->AbsoluteUri->Data();
auto action = m_dispatcher->RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
ref new Windows::UI::Core::DispatchedHandler([this]()
{
auto beginUri = ref new Uri(ref new String(m_authurl.c_str()));
task<WebAuthenticationResult^>authTask(
WebAuthenticationBroker::AuthenticateAsync(
WebAuthenticationOptions::None, beginUri));
authTask.then([this](WebAuthenticationResult^ result)
{
String^ statusString;
switch(result->ResponseStatus)
{
case WebAuthenticationStatus::Success:
{
auto actionEnable = m_dispatcher->RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
ref new Windows::UI::Core::DispatchedHandler([this]()
{
UploadFileBtn->IsEnabled = true;
}));
}
}
});
}));
}
Затем я вызываю функцию OAuthLoginAsync, которая выполняет операцию входа в Dropbox. Как только эта асинхронная функция возвращает управление, я использую функцию RunAsync объекта Dispatcher для маршалинга вызова обратно в UI-поток из фонового потока асинхронной задачи. Функция RunAsync принимает два параметра: значение приоритета и экземпляр DispatchedHandler. Я задаю приоритет как «Normal» и передаю функцию лямбды экземпляру DispatchedHandler. В теле лямбды я вызываю статическую функцию AuthenticateAsync класса WebAuthenticationBroker, которая потом отображает модальный диалог приложения и помогает выполнить защищенную аутентификацию.
При написании приложений Windows Store приходится учитывать некоторые ограничения, с которыми вы должны смириться.
По окончании рабочего процесса диалог удаляется, и функция возвращает либо код успешного завершения, либо обнаруженные ошибки. В моем случае я просто обрабатываю возвращаемый тип WebAuthenticationStatus::Success и снова использую объект диспетчера, чтобы сделать доступной кнопку UploadFile в UI. Поскольку все вызываемые мной функции являются асинхронными, мне нужно задействовать объект диспетчера для маршалинга вызовов в UI-поток, если я хочу обращаться к каким-либо UI-элементам.
Обработчик событий UploadFileBtnClicked показан на рис. 7. В самом обработчике кода не так уж много. Я вызываю функцию FileOpenPicker::PickSingleFileAsync, которая позволяет выбрать один текстовый файл через интерфейс выбора (picker interface). Затем вызываю функцию OAuthAcquireTokenAsync (рис. 8) и при успешном завершении обращаюсь к функции UploadFileToDropBoxAsync (рис. 9).
Рис. 7. Функция UploadFileBtnClicked
void MainPage::UploadFileBtnClicked( Platform::Object^ sender,
RoutedEventArgs^ e)
{
using namespace Windows::Storage::Pickers;
using namespace Windows::Storage;
auto picker = ref new FileOpenPicker();
picker->SuggestedStartLocation = PickerLocationId::DocumentsLibrary;
picker->FileTypeFilter->Append(".txt");
task<StorageFile^> (picker->PickSingleFileAsync())
.then([this](StorageFile^ selectedFile)
{
m_fileToUpload = selectedFile;
OAuthAcquireTokenAsync(m_creds).then([this](){
UploadFileToDropBoxAsync(m_creds);
});
});
}
Рис. 8. Функция OAuthAcquireTokenAsync
task<void> MainPage::OAuthAcquireTokenAsync(
std::shared_ptr<AppCredentials>& creds)
{
uri url(DropBoxAccessTokenURI);
std::shared_ptr<OAuth> oAuthObj = std::make_shared<OAuth>();
auto signatureParams =
oAuthObj->CreateOAuthSignedParameters(url.to_string(),
L"GET",
NULL,
consumerKey,
consumerSecret,
creds->Token(),
creds->TokenSecret()
);
std::wstring sb = oAuthObj->OAuthBuildSignedHeaders(url);
http_client client(sb);
// Выдаем запрос и асинхронно обрабатываем ответ
return client.request(methods::GET)
.then([&creds](http_response response)
{
if(response.status_code() != status_codes::OK)
{
auto stream = response.body();
container_buffer<std::string> inStringBuffer;
return stream.read_to_end(inStringBuffer)
.then([inStringBuffer](pplx::task<size_t> previousTask)
{
UNREFERENCED_PARAMETER(previousTask);
const std::string &text = inStringBuffer.collection();
// Преобразуем текст ответа в широкосимвольную строку
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>,
wchar_t> utf16conv;
std::wostringstream ss;
ss << utf16conv.from_bytes(text.c_str()) << std::endl;
OutputDebugString(ss.str().data());
// Обработка ошибок
return pplx::task_from_result();
});
}
// Здесь выполняем операции, читая из потока ответа
istream bodyStream = response.body();
container_buffer<std::string> inStringBuffer;
return bodyStream.read_to_end(inStringBuffer)
.then([inStringBuffer, &creds](pplx::task<size_t> previousTask)
{
UNREFERENCED_PARAMETER(previousTask);
const std::string &text = inStringBuffer.collection();
// Преобразуем текст ответа в широкосимвольную строку
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>,
wchar_t> utf16conv;
std::wostringstream ss;
std::vector<std::wstring> parts;
ss << utf16conv.from_bytes(text.c_str()) << std::endl;
Split(ss.str(), parts, '&', false);
unsigned pos = parts[1].find('=');
std::wstring token = parts[1].substr(pos + 1, 16);
pos = parts[0].find('=');
std::wstring tokenSecret = parts[0].substr(pos + 1);
creds->SetToken(token);
creds->SetTokenSecret(tokenSecret);
});
});
}
Рис. 9. Функция UploadFileToDropBoxAsync
task<void> MainPage::UploadFileToDropBoxAsync(
std::shared_ptr<AppCredentials>& creds)
{
using concurrency::streams::file_stream;
using concurrency::streams::basic_istream;
uri url(DropBoxFileUploadURI);
std::shared_ptr<oAuth> oAuthObj = std::make_shared<oAuth>();
auto signatureParams =
oAuthObj->CreateOAuthSignedParameters(url.to_string(),
L"PUT",
NULL,
consumerKey,
consumerSecret,
creds->Token(),
creds->TokenSecret()
);
std::wstring sb = oAuthObj->OAuthBuildSignedHeaders(url);
return file_stream<unsigned char>::open_istream(this->m_fileToUpload)
.then([this, sb, url](pplx::task<basic_istream<unsigned char>> previousTask)
{
try
{
auto fileStream = previousTask.get();
// Получаем длину контента,
// присвоенного свойству Content-Length
fileStream.seek(0, std::ios::end);
auto length = static_cast<size_t>(fileStream.tell());
fileStream.seek(0, 0);
// Выдаем HTTP-запрос с файловым потоком в качестве тела
http_request req;
http_client client(sb);
req.set_body(fileStream, length);
req.set_method(methods::PUT);
return client.request(req)
.then([this, fileStream](pplx::task<http_response> previousTask)
{
fileStream.close();
std::wostringstream ss;
try
{
auto response = previousTask.get();
auto body = response.body();
// Протоколируем код успешного ответа
ss << L"Server returned status code "
<< response.status_code() << L"."
<< std::endl;
OutputDebugString(ss.str().data());
if (response.status_code() == web::http::status_codes::OK)
{
auto action = m_dispatcher->RunAsync(
Windows::UI::Core::CoreDispatcherPriority::Normal,
ref new Windows::UI::Core::DispatchedHandler([this]()
{
using namespace Windows::UI::Popups;
auto msgDlg = ref new MessageDialog(
"File uploaded successfully to Dropbox");
msgDlg->ShowAsync();
}));
}
}
catch (const http_exception& e)
{
ss << e.what() << std::endl;
OutputDebugString(ss.str().data());
}
});
}
catch (const std::system_error& e)
{
// Здесь протоколируем любые ошибки
// и возвращаем пустую задачу
std::wostringstream ss;
ss << e.what() << std::endl;
OutputDebugString(ss.str().data());
return pplx::task_from_result();
}
});
}
Функция OAuthAcquireTokenAsync получает маркер, сопоставленный с учетной записью Dropbox. Сначала я формирую необходимую строку доступа (access string) и заголовки HTTP-запроса и вызываю сервис Dropbox для проверки удостоверений. Этот HTTP-запрос имеет тип GET, а ответ возвращается как поток символов. Я разбираю этот поток для получения самого маркера и секрета, которые потом сохраняются в экземпляре класса AppCredentials.
Успешно получив маркер и его секрет от Dropbox, я использую их для загрузки файла в Dropbox. Как и в случае любой конечной точки веб-доступа к Dropbox, сначала формируется строка параметров и HTTP-заголовки. Затем вызывается конечная точка сервиса Dropbox, сопоставленная с загрузкой файлов. Этот HTTP-запрос имеет тип PUT, поскольку я пытаюсь поместить контент в сервис. Перед этим мне также нужно сообщить Dropbox о размере контента. Это указывается установкой значения свойства content_length в методе HTTP_request::set_body равным размеру загружаемого файла. После успешного возврата PUT-метода с помощью объекта диспетчера я вывожу пользователю сообщение об успешном завершении операции.
На очереди Linux
Интеграция C++ REST SDK в приложения Windows 8 (как Windows Store, так и настольные) проста и прямолинейна. Добавьте преимущества написания кода, который может быть общим между двумя платформами, применение идиом программирования на современном C++ и тот факт, что данный код является портируемым между приложениями как Windows, так и других ОС, — и приз ваш. Вы можете больше не беспокоиться о специфичных для платформ тонкостях, относящихся к сетевым API, и вместо этого уделять больше времени продумыванию функциональности, которую должно поддерживать ваше приложение. В этом простом примере я задействовал C++ REST SDK для аутентификации пользователя в Dropbox и последующей загрузки файла в облако Dropbox. Подробнее о Dropbox REST API см. документацию по ссылке bit.ly/10OdTD0. В следующей статье я покажу, как выполнять те же задачи из Linux-клиента.
Сридхар Подури (Sridhar Poduri) — менеджер программ в группе Windows в Microsoft. Страстный поклонник C++ и автор книги «Modern C++ and Windows Store Apps» (Sridhar Poduri, 2013), регулярно пишет о C++ и Windows Runtime в своем блоге sridharpoduri.com.
Выражаю благодарность за рецензирование статьи экспертам Microsoft Никласу Густаффсону (Niklas Gustaffson), Сана Митани (Sana Mithani) и Огги Шобахичу (Oggy Sobajic).