C++
在 Windows 应用商店应用程序中使用 C++ REST SDK
在我的上一篇文章 (msdn.microsoft.com/magazine/dn342869) 中,我介绍了 C++ REST SDK 以及如何在 Win32/MFC 应用程序中使用它。 在本文中,我将探讨如何将 C++ REST SDK 集成到 Windows 应用商店应用程序中。 我最初使用 C++ REST SDK 和 OAuth 身份验证类的目标之一是尽可能采用标准 C++,并且只在必要时才与特定于平台的 API 进行交互。 下面简要回顾了上一篇文章的内容:
- OAuth 身份验证类中使用的代码采用标准 C++ 类型,不使用特定于 Windows 的类型。
- 用于向 Dropbox REST 服务发出 Web 请求的代码使用 C++ REST SDK 中的类型。
- 唯一特定于平台的代码是在 Dropbox 应用程序控制台门户上启动 Internet Explorer 并完成应用程序身份验证和批准的函数。
对于我的 Windows 应用商店应用程序,我的目标一样,即为身份验证提供支持,同时将文件上载到 Dropbox。 我设法尽可能多地提供可移植的 C++ 代码,并只在必要时才与 Windows 运行时 (WinRT) 进行交互。 大家可以从 archive.msdn.microsoft.com/mag201308CPP 下载两篇文章的示例代码。
Win32 解决方案的问题
之前的 Win32 应用程序有一个巨大的缺陷,即需要启动外部应用程序才能完成 OAuth 授权过程。 这意味着,我必须启动 Internet Explorer(您也可以启动自己偏好的浏览器),使用我的凭据登录 Dropbox,然后完成此工作流。 如图 1 和图 2 所示。
图 1 先使用我的凭据登录 Dropbox,再授权应用程序进行访问
图 2 在 Dropbox 门户上成功为我的应用程序授权
大家可以看到,通过启动外部应用程序并提示用户通过该外部应用程序完成工作流,焦点会离开我的应用程序。 作为一名开发人员,我也没有标准的机制来在工作流完成时通知我的应用程序。 我一直专注于异步编程,并且 C++ REST SDK 支持基于异步任务的编程,不得不启动外部程序的做法无疑让我感到十分沮丧。 我试着使用命名管道、内存映射文件等方法,但它们全都需要编写另外一个应用程序来托管 Web 控件实例,然后通过命名管道、共享内存或内存映射文件写回成功的值。 我最终决定使用浏览器执行此任务,因为我不想编写另一个应用程序来包装浏览器控件。
与 Windows 运行时集成
在开始设计我的应用程序来支持 Windows 运行时的时候,我考虑了几种方案。 我将在下面简要列出这些方案,并详细讨论最终选定的方法:
- 采用协议激活,并通过调用 Windows::System::Launcher::LaunchUriAsync 函数让系统启动适当的进程来处理协议。 这意味着,对于基于 HTTPS 的 URI,操作系统将启动默认浏览器。 这与从 Win32 示例中启动 Internet Explorer 类似,但增加了一个问题:我的 Windows 应用商店应用程序将被推送到后台,默认浏览器将全屏启动,在最糟的情况下,我的应用程序会在用户完成工作流时挂起。 这种方法切不可行。
- 在我的应用程序中集成 WebView 控件。 通过使用 XAML WebView 控件,我可以将整个工作流导航嵌入在应用程序的上下文中。 从理论上讲,通过侦听由 WebView 控件激发的 window.external.notify 事件, 我还可以在进程完成后收到通知。 但实际上,只有网页激发通知事件时,才会激发该事件。 然而,在我的例子中,完成授权过程的 Dropbox 页面并不会激发该事件。 郁闷!
- 在我的应用程序中使用 WebAuthenticationBroker。 在围绕 Windows 运行时寻找出路时,我试着使用了 WebAuthenticationBroker 类。 它看上去能够帮助我完成授权过程,而且似乎只需编写几行代码,即可让整个功能发挥作用。 在深入介绍代码之前,我先略微详细地说明一下 WebAuthenticationBroker。
WebAuthenticationBroker
在彼此互连的应用程序世界中,需要通过可信、安全的机制提示用户提供凭据,以获得用户许可并对应用程序授权,这一点非常重要。 没有人愿意开发出泄露用户凭据的应用程序,或者成为拦截用户信息的偷窃攻击的受害者。 Windows 运行时包括一系列 API 和必要的技术,可供开发人员以安全、可信的方式索取用户凭据。 Windows 应用商店应用程序可借助多种工具来使用基于 Internet 的身份验证和授权协议(如 OAuth 和 OpenID),WebAuthenticationBroker 便是这类工具之一。 在我的 Dropbox 示例应用程序中,它是如何发挥作用的? 以下是具体方式:
- 我向 Dropbox 发出初始异步请求,后者返回一个令牌和令牌密钥供我的应用程序使用。 此初始请求通过函数 oAuthLoginAsync 发出。
- 当 oAuthLoginAsync 函数返回时,为延续这一序列,我构造了应从中开始授权过程的 URI。 在我的示例中,我已经将初始 URI 定义为一个常量字符串:
const std::wstring DropBoxAuthorizeURI =
L"https://www.dropbox.com/1/oauth/authorize?oauth_token=";
- 随后,通过追加 Dropbox 返回的令牌,我构建了 HTTP 请求 URI。
- 作为额外的一步,我又通过调用 WebAuthenticationBroker::GetCurrentApplicationCallbackUri 函数构造了回调 URI 参数。 请注意,在我的桌面应用程序中,我没有使用回调 URI 参数,因为该回调参数是可选的,我当时依靠 Internet Explorer 来执行授权任务。
- 现在,我的请求字符串已经准备好,下面可以发出请求了。 我没有使用 C++ REST SDK http_client 类或 IHttpWebRequest2 接口来调用 Web 服务,而是调用 WebAuthenticationBroker::AuthenticateAsync 函数。
- WebAuthenticationBroker::AuthenticateAsync 函数接受两个参数:一个 WebAuthenticationOptions 枚举和一个 URI。 该函数的重载实例接受 WebAuthenticationOptions 枚举和两个 URI,分别作为身份验证过程开始的起始 URI 和身份验证过程结束的终止 URI。
- 我使用第一个版本的 AuthenticateAsync 函数,并为 WebAuthenticationOptions 枚举传递 None 值。 对于 URI,我传递为我的 Web 请求构建的 URI。
- WebAuthenticationBroker 介于我的应用程序与系统之间。 当我调用 AuthenticateAsync 时,它会创建一个系统模式对话框来作为我的应用程序的模式。
- 此代理还会在它创建的模式对话框上附加一个 Web 主机窗口。
- 随后,此代理会选择一个专用应用程序容器进程,此容器独立于执行我的应用程序的应用程序容器。 它还会清除我的应用程序中的所有持久性数据。
- 接下来,此代理会在这个新选择的应用程序容器中启动身份验证过程,并导航至 AuthenticateAsync 函数中指定的 URI。
- 当用户与网页交互时,此代理会持续检查每个 URL 中是否有指定的回调 URI。
- 一旦找到匹配项,Web 主机将结束导航并向代理发送信号。 代理将关闭对话框,从应用程序容器中清除由 Web 主机创建的任何持久性 Cookie,然后将协议数据返回给应用程序。
图 3 显示了在 Web 主机导航到初始 URI 之后,我的示例 Dropbox 应用程序中出现的 WebAuthenticationBroker 模式对话框。 由于 Dropbox 要求用户先登录才能显示授权页面,因此 Web 主机将导航页面重定向至 Dropbox 登录页面。
图 3 模式对话框中显示的 Dropbox 登录页面
一旦用户登录 Dropbox,Web 主机将导航至实际的授权 URI。 如图 4 所示。 从图 3 和图 4 中可以明显看出,该对话框叠加在我的应用程序 UI 之上。 无论调用 WebAuthenticationBroker::AuthenticateAsync 方法的源应用程序为何,UI 都将保持一致。 由于始终能够获得一致的体验,因此用户可以放心提供凭据信息,而无需担心处理凭据信息的应用程序会意外泄露这些信息。
图 4 Dropbox 提示用户是否同意授权应用程序
上面没有提到一件很重要的事,即需要从 UI 线程调用 WebAuthenticationBroker::AuthenticateAsync 函数。 所有 C++ REST SDK Web 请求都在后台线程上发出,我无法从后台线程中显示此 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;
}));
}
}
});
}));
授权过程完成后,我再次运行该调度程序来启用主 UI 中的“Upload File”按钮。 在用户对我的应用程序完成身份验证并授权其访问 Dropbox 之前,此按钮一直处于禁用状态。
链接异步 Web 请求
现在可轻松将 Web 请求链接在一起。 在不与 Windows 运行时交互的所有函数中,我重复使用了桌面应用程序中的相应代码。 我没有对代码进行大量更改,只有一点例外:我决定在 UploadFileToDropboxAsync 函数中使用 WinRT StorageFile 对象而非 C++ iostream。
在为 Windows 应用商店编写应用程序时,您需要接受一些限制。 其中一项限制是需要使用 WinRT StorageFile 对象代替 C++ streams 在文件中读写数据。 在使用 C++ REST SDK 开发 Windows 应用商店应用程序时,所有与文件相关的操作都要求开发人员传递 StorageFile 对象而非 C++ stream 对象。 通过这项细微改动,我可以在 Windows 应用商店示例应用程序中重复使用支持 OAuth 和 Dropbox 授权代码的所有标准 C++ 代码。
以下是我的伪代码流(在伪代码后,我将讨论各个函数):
On clicking the SignIn Button
Call oAuthLoginAsync function
Then call WebAuthenticationBroker::AuthenticateAsync
Then enable the "Upload File" button on my UI
On clicking the "Upload File" button
Call the Windows::Storage::Pickers::FileOpenPicker::
PickSingleFileAsync function
Then call oAuthAcquireTokenAsync function
Then call UploadFileToDropboxAsync function
在图 6 显示的 SignInBtnClicked 按钮事件处理程序中,我先执行简单的参数验证,确保不向传递给 Dropbox 进行身份验证的 ConsumerKey 和 ConsumerSecret 参数传递空字符串值。 接下来,我获取与 CoreWindow 当前线程关联的 Dispatcher 对象的实例,并将其存储在 MainPage 类的成员变量中。 Dispatcher 负责处理窗口消息并向应用程序调度事件。 下面,我创建 OnlineIdAuthenticator 类的实例。 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 执行登录操作。 此异步函数返回后,我使用 Dispatcher 对象的 RunAsync 函数从异步任务的后台线程封送对 UI 线程的回调。 RunAsync 函数有两个参数:一个优先级值和一个 DispatchedHandler 实例。 我将优先级值设置为 Normal,并向 DispatchedHandler 实例传递 lambda 函数。 在 lambda 函数体内,我调用 WebAuthenticationBroker 类的静态函数 AuthenticateAsync,后者显示应用程序模式对话框并帮助完成安全授权。
当工作流完成后,对话框将关闭,此函数会返回指示成功完成的消息或遇到的任何错误情况。 在我的例子中,我只处理 WebAuthenticationStatus::Success 返回类型,并再次使用 Dispatcher 对象启用应用程序 UI 中的“UploadFile”按钮。 由于我调用的所有函数本质上都是异步函数,因此如果要访问任何 UI 元素,都需要使用 Dispatcher 对象封送对 UI 线程的调用。
图 7 显示了 UploadFileBtnClicked 事件处理程序。 该处理程序本身没有太多代码。 我调用 FileOpenPicker::PickSingleFileAsync 函数,以通过选取器界面选择单个文本文件。 随后,我调用 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);
// Make the request and asynchronously process the response.
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();
// Convert the response text to a wide-character string.
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());
// Handle error cases.
return pplx::task_from_result();
});
}
// Perform actions here reading from the response stream.
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();
// Convert the response text to a wide-character string.
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();
// Get the content length, used to set the Content-Length property.
fileStream.seek(0, std::ios::end);
auto length = static_cast<size_t>(fileStream.tell());
fileStream.seek(0, 0);
// Make HTTP request with the file stream as the body.
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();
// Log response success code.
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)
{
// Log any errors here.
// Return an empty task.
std::wostringstream ss;
ss << e.what() << std::endl;
OutputDebugString(ss.str().data());
return pplx::task_from_result();
}
});
}
OAuthAcquireTokenAsync 函数会执行操作,以获取与 Dropbox 帐户关联的实际令牌。 我先构建了所需的访问字符串和 HTTP 请求头,然后调用 Dropbox 服务执行凭据验证。 此 HTTP 请求属于 GET 类型,返回的响应为一个字符流。 我对此字符流进行了分析和拆分,以获取实际令牌和令牌密钥值。 这些值随后存储在 AppCredentials 类实例中。
在从 Dropbox 成功获取实际令牌和令牌密钥值后,下面即可通过向 Dropbox 上载文件来使用它们。 我首先构建参数字符串和 HTTP 头,这是所有 Dropbox Web 终结点访问的规范。 随后,我调用与上载文件关联的 Dropbox 服务终结点。 此 HTTP 请求属于 PUT 类型,因为我在试图向该服务中添加内容。 添加内容之前,我还需要让 Dropbox 了解内容的大小。 为此,我将 HTTP_request::set_body 方法的 content_length 属性设置为待上载文件的大小。 在 PUT 方法成功返回后,我使用 Dispatcher 对象向用户显示成功消息。
接下来谈 Linux
在 Windows 8 应用程序(包括 Windows 应用商店和桌面)中集成 C++ REST SDK 非常简单。 这样做具有诸多好处,比如编写的代码可在两种平台之间共享,采用现代 C++ 编程术语,以及代码可在 Windows 和非 Windows 应用程序之间移植等,这将有助于您获胜。 您不必再担心与网络 API 相关的特定于平台的难题,而可以用这些时间来思考您的应用程序需要支持哪些功能。 在此简单的示例中,我使用 C++ REST SDK 在 Dropbox 中验证用户身份,然后向 Dropbox 云上载了一个文件。 有关 Dropbox REST API 的更多信息,请参阅 bit.ly/10OdTD0 上的文档。 在后续文章中,我将介绍如何在 Linux 客户端上完成相同的任务。
Sridhar Poduri 是 Microsoft Windows 团队的一名项目经理。他是一位 C++ 迷,著有《Modern C++ and Windows Store Apps》(Sridhar Poduri,2013 年)一书,经常在 sridharpoduri.com 上就 C++ 和 Windows 运行时发表博文。
衷心感谢以下 Microsoft 技术专家对本文的审阅:Niklas Gustaffson、Sana Mithani 和 Oggy Sobajic