C++

在 Windows 应用商店应用程序中使用 C++ REST SDK

Sridhar Poduri

下载代码示例

在我的上一篇文章 (msdn.microsoft.com/magazine/dn342869) 中,我介绍了 C++ REST SDK 以及如何在 Win32/MFC 应用程序中使用它。 在本文中,我将探讨如何将 C++ REST SDK 集成到 Windows 应用商店应用程序中。 我最初使用 C++ REST SDK 和 OAuth 身份验证类的目标之一是尽可能采用标准 C++,并且只在必要时才与特定于平台的 API 进行交互。 下面简要回顾了上一篇文章的内容:

  1. OAuth 身份验证类中使用的代码采用标准 C++ 类型,不使用特定于 Windows 的类型。
  2. 用于向 Dropbox REST 服务发出 Web 请求的代码使用 C++ REST SDK 中的类型。
  3. 唯一特定于平台的代码是在 Dropbox 应用程序控制台门户上启动 Internet Explorer 并完成应用程序身份验证和批准的函数。

对于我的 Windows 应用商店应用程序,我的目标一样,即为身份验证提供支持,同时将文件上载到 Dropbox。 我设法尽可能多地提供可移植的 C++ 代码,并只在必要时才与 Windows 运行时 (WinRT) 进行交互。 大家可以从 archive.msdn.microsoft.com/mag201308CPP 下载两篇文章的示例代码。

Win32 解决方案的问题

之前的 Win32 应用程序有一个巨大的缺陷,即需要启动外部应用程序才能完成 OAuth 授权过程。 这意味着,我必须启动 Internet Explorer(您也可以启动自己偏好的浏览器),使用我的凭据登录 Dropbox,然后完成此工作流。 如图 1图 2 所示。

Logging in to Dropbox Using My Credentials Before Authorizing Application Access
图 1 先使用我的凭据登录 Dropbox,再授权应用程序进行访问

Successful Authorization for My Application on the Dropbox Portal
图 2 在 Dropbox 门户上成功为我的应用程序授权

大家可以看到,通过启动外部应用程序并提示用户通过该外部应用程序完成工作流,焦点会离开我的应用程序。 作为一名开发人员,我也没有标准的机制来在工作流完成时通知我的应用程序。 我一直专注于异步编程,并且 C++ REST SDK 支持基于异步任务的编程,不得不启动外部程序的做法无疑让我感到十分沮丧。 我试着使用命名管道、内存映射文件等方法,但它们全都需要编写另外一个应用程序来托管 Web 控件实例,然后通过命名管道、共享内存或内存映射文件写回成功的值。 我最终决定使用浏览器执行此任务,因为我不想编写另一个应用程序来包装浏览器控件。

与 Windows 运行时集成

在开始设计我的应用程序来支持 Windows 运行时的时候,我考虑了几种方案。 我将在下面简要列出这些方案,并详细讨论最终选定的方法:

  1. 采用协议激活,并通过调用 Windows::System::Launcher::LaunchUriAsync 函数让系统启动适当的进程来处理协议。 这意味着,对于基于 HTTPS 的 URI,操作系统将启动默认浏览器。 这与从 Win32 示例中启动 Internet Explorer 类似,但增加了一个问题:我的 Windows 应用商店应用程序将被推送到后台,默认浏览器将全屏启动,在最糟的情况下,我的应用程序会在用户完成工作流时挂起。 这种方法切不可行。
  2. 在我的应用程序中集成 WebView 控件。 通过使用 XAML WebView 控件,我可以将整个工作流导航嵌入在应用程序的上下文中。 从理论上讲,通过侦听由 WebView 控件激发的 window.external.notify 事件, 我还可以在进程完成后收到通知。 但实际上,只有网页激发通知事件时,才会激发该事件。 然而,在我的例子中,完成授权过程的 Dropbox 页面并不会激发该事件。 郁闷!
  3. 在我的应用程序中使用 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::GetCurrent­ApplicationCallbackUri 函数构造了回调 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 应用程序中出现的 Web­AuthenticationBroker 模式对话框。 由于 Dropbox 要求用户先登录才能显示授权页面,因此 Web 主机将导航页面重定向至 Dropbox 登录页面。

The Sign-in Page of Dropbox as Displayed in the Modal Dialog
图 3 模式对话框中显示的 Dropbox 登录页面

一旦用户登录 Dropbox,Web 主机将导航至实际的授权 URI。 如图 4 所示。 从图 3图 4 中可以明显看出,该对话框叠加在我的应用程序 UI 之上。 无论调用 WebAuthenticationBroker::Authen­ticateAsync 方法的源应用程序为何,UI 都将保持一致。 由于始终能够获得一致的体验,因此用户可以放心提供凭据信息,而无需担心处理凭据信息的应用程序会意外泄露这些信息。

User Consent Being Asked for Application Authorization from Dropbox
图 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