65.9K
CodeProject 正在变化。 阅读更多。
Home

Google OAuth2 on Windows Phone

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (10投票s)

2012年1月29日

CPOL

7分钟阅读

viewsIcon

66922

downloadIcon

1648

在Windows Phone 7上使用Google API和OAuth2身份验证的示例。

引言

Google 提供了丰富的 RESTful API,允许应用与其应用程序、服务和数据进行交互。虽然 Google 支持多种身份验证和授权机制,但他们推荐使用OAuth2,所以我决定探索如何在 WP7 上实现它。

如果你和我一样,在尝试使用 Google 服务时,会阅读 OAuth2 的相关资料,查看一些示例代码,希望能够找到一些可以直接集成到应用程序中的东西,然后继续处理有趣的部分。虽然关于 OAuth2 和如何使用它的示例有很多文档,甚至还有Google 提供的 .NET 库,但仍有一些微妙之处需要集成到应用程序中以处理多种不同的用例。我还没有找到对其中一些细节的详细解释,或者将其以良好封装的方式集成到应用程序中的好方法。

  • 针对已安装应用程序的 OAuth2 是一个多步骤过程。应用程序不捕获用户的用户名和密码,而是显示一个嵌入式网页进行身份验证和授权。成功后,应用程序会收到一个代码,该代码必须用于交换访问令牌。
  • 身份验证是同步发生的,但通信本质上是异步的。
  • 访问令牌带有过期时间。当它过期时,无需用户重新进行身份验证即可刷新,但这确实需要应用程序向 Google 发出刷新调用。
  • 访问令牌可以序列化,以便应用程序的新实例无需重新进行身份验证。
  • 用户可以随时在应用程序环境之外撤销应用程序代表他们访问 Google 服务的权限。

我编写此代码的目标是首先理解 OAuth2 及其在 Google 中的应用,同时也创建一些代码,将授权何时以及如何发生的细节隐藏起来,不让应用程序的其他部分知道。因为需要重新验证或刷新访问令牌可能随时发生,我希望让应用程序代码尽可能自然地使用 RESTful 服务,而无需了解这些细节。

这个示例应用程序所做的只是允许用户使用 Google 进行身份验证,授权应用程序访问他们的基本个人资料数据,然后显示已验证用户的个人资料数据。然而,它确实演示了 OAuth2 的基本原理,并希望能提供一些在 WP7 应用程序中实现它的示例。

背景

首先,如果你从未接触过 Google API,最好对它们的服务工作原理和 OAuth2 已安装应用程序的实现有一个基本的了解。如果你要创建一个使用 Google 服务的应用程序,你需要通过 它们的 API 控制台进行注册。在那里你可以获得下面引用的 ClientId 和密钥,并获得应用程序使用特定 Google 服务的权限。

随附代码使用

你可以通过 NuGet 获取所有这些包。(截至本文撰写之时,最新版本的 RestSharp (102.6) 与最新版本的 JSON.NET (4.0.7) 不兼容,因此你可能需要确保获取 JSON.NET 4.05)。

Using the Code

注意

为了运行此演示,你需要注册Google Code并获取客户端 ID 和密钥,然后将其输入到AuthenticationViewModel构造函数中。

#warning PUT YOUR APP SPECIFIC STUFF HERE
            _process = new AuthenticationProcess()
            {
                ClientId = "YOUR APP CLIENT ID",
                Secret = "YOUR APP SECRET",

                // this specifies which Google APIs your app 
                // intends to use and needs permission for
                Scope = "https://www.googleapis.com/auth/userinfo.email 
                         https://www.googleapis.com/auth/userinfo.profile"
            };

OAuth2 序列

基本的 OAuth2 序列是用户、应用程序和 Google 之间的一系列请求/响应

321291/nativeflow.png

如你所见,用户实际上并不与你的应用程序交互来进行登录和授权。用户直接与 Google 交互,但你的应用程序仍然需要知道交互结果才能继续进行。

在这个示例应用程序中,它是一个 MVVM 应用程序,将身份验证绑定到应用程序的逻辑由 AuthenticationViewModel 类处理。与 Google 之间的来回通信则在 AuthenticationProcess 类中。

你会注意到的一件事是,在代码中(除了身份验证视图模型之外),你不会看到任何启动登录进程的代码。任何其他视图模型唯一需要确保做的就是异步地从身份验证视图模型获取访问代码。只要它这样做,就可以假设应用程序已通过身份验证,或者至少在其回调被调用时将通过身份验证。

这解决了我在其他 Rest 客户端应用程序中遇到的一个问题:控制入口点,以便在正确的时间正确地进行身份验证,并确保客户端访问了解当前的身份验证状态。

此示例中的 OAuth2

由于这是一个 MVVM Light 应用程序,它带有一个 ViewModelLocator、一个 MainPage 和一个 MainViewModelMainPage 是应用程序启动时导航到的页面,其 UI 的一部分绑定到 MainViewModel 上一个名为 Profile 的属性,该属性保存用户的 Google 个人资料数据。当 UI 绑定到该属性时,MainViewModel 会在从 AuthenticationViewModel 检索访问令牌的过程中加载个人资料数据。在这个示例中,认证视图模型在其构造函数中传递给 MainViewModel,并且是 _authProvider 成员。

public Profile Profile
{
    get
    {
        if(_profile == null)
            _authProvider.GetAccessCode(s => LoadProfile(s));

        return _profile;
    }
    set
    {
        if (_profile != value)
        {
            _profile = value;
            RaisePropertyChanged("Profile");
        }
    }
}

调用 AuthenticationViewModel::GetAccessCode 是封装身份验证、授权、刷新等逻辑的地方。

private Queue<Action<string>> _queuedRequests = new Queue<Action<string>>();
public void GetAccessCode(Action<string> callback)
{
    lock (_sync)
    {
        if (_isAuthenticating)
        {
            _queuedRequests.Enqueue(callback);
        }
        else if (HasAuthenticated)
        {
            if (!_process.AuthResult.IsExpired)
            {
                callback(_process.AuthResult.access_token);
            }
            else
            {
                _isAuthenticating = true;
                _queuedRequests.Enqueue(callback);
                _process.RefreshAccessToken();
            }
        }
        else
        {
            _isAuthenticating = true;
            _queuedRequests.Enqueue(callback);

            ((PhoneApplicationFrame)App.Current.RootVisual).Navigate
                       (new Uri("/AuthenticationPage.xaml", UriKind.Relative));
            AuthUri = _process.AuthUri;
        }
    }
}

该方法中发生了几件事

  • 首先,如果身份验证已经在进行中,则将回调排队并返回
  • 如果用户已通过身份验证且访问令牌有效,则直接调用回调。
  • 如果用户已通过身份验证但访问令牌已过期,则将回调排队并刷新令牌
  • 如果用户尚未成功通过身份验证,则将回调排队并导航到身份验证页面。

按照最后一个块的流程,这在应用程序首次启动时会发生,用户将被导航到身份验证页面,该页面显示一个指向身份验证视图模型的 AuthUri 属性的 web 浏览器。

AuthUri 是用户登录的 Google 页面,它将类似于 https://#/o/oauth2/auth?response_type=code&redirect_uri=https://&scope=https://www.googleapis.com/auth/userinfo.email%20https://www.googleapis.com/auth/userinfo.profile&client_id=YYYY

321291/login.PNG

在通过 Google 进行身份验证后,他们将被要求授权您的应用程序访问一组已定义的 API,这些 API 分别由上述 URI 中的 client_idscope 参数定义。

321291/authorize.PNG

用户按下“允许访问”后,Google 会将浏览器重定向到我们在起始地址中传递的 redirect_uri。当 AutenticationPage 代码后台检测到网络浏览器正在访问原始地址中 redirect_uri 参数指定的主机时,我们知道 Google 正在将访问代码传递回来,我们可以将其交换为访问令牌。访问代码是重定向地址查询字符串的一部分。我们从不实际导航到 redirect_uri,而是将其用作哨兵值,以便我们知道身份验证何时成功。

    private void webBrowser1_Navigating(object sender, NavigatingEventArgs e)
    {
        if (e.Uri.Host.Equals("localhost")) // in our case we used localhost as the redirect_uri
        {
            webBrowser1.Visibility = Visibility.Collapsed;
            e.Cancel = true;
            int pos = e.Uri.Query.IndexOf("=");

            // setting this ui element text will bind it back to the view model
            codeBlock.Text = pos > -1 ? e.Uri.Query.Substring(pos + 1) : null;
        }
    }

一旦代码被发送回身份验证视图模型(在这种情况下,因为我们有一个带有双向绑定的隐藏文本块),视图模型会将其交换为访问令牌

    private string _code;
    public string Code
    {
        get
        {
            return _code;
        }
        set
        {
            _code = value;
            _process.ExchangeCodeForToken(Code);
        }
    }
 
    ...

    class AuthenticationProcess
    {
        public void ExchangeCodeForToken(string code)
        {
            if (string.IsNullOrEmpty(code))
            {
                OnAuthenticationFailed();
            }
            else
            {
                var request = new RestRequest(this.TokenEndPoint, Method.POST);
                request.AddParameter("code", code);
                request.AddParameter("client_id", this.ClientId);
                request.AddParameter("client_secret", this.Secret);
                request.AddParameter("redirect_uri", "https://");
                request.AddParameter("grant_type", "authorization_code");

                client.ExecuteAsync<AuthResult>(request, GetAccessToken);
            }
        }

        void GetAccessToken(IRestResponse<AuthResult> response)
        {
            if (response == null || response.StatusCode != HttpStatusCode.OK
                || response.Data == null || string.IsNullOrEmpty(response.Data.access_token))
            {
                OnAuthenticationFailed();
            }
            else
            {
                Debug.Assert(response.Data != null);
                AuthResult = response.Data;
                OnAuthenticated();
            }
        }

    }

此时,AuthenticationProcess 类向身份验证视图模型发出信号,表示身份验证成功,视图模型调用所有排队的回调,进行一些清理,然后过程完成。

    void _process_Authenticated(object sender, EventArgs e)
    {
        _isAuthenticating = false;

        while (_queuedRequests.Count > 0)
            _queuedRequests.Dequeue()(_process.AuthResult.access_token);

        ViewModelLocator.SaveSetting("auth", _process.AuthResult);        

        RaisePropertyChanged("HasAuthenticated");
    }

然后,回调,一路返回到 MainViewModel,将使用有效的访问令牌调用,我们可以继续我们来这里要做的事情:调用 Google API,将访问令牌传递给 RestSharp 身份验证器。

    private void LoadProfile(string access_token)
    {
        Debug.WriteLine("loading profile");

        RestClient client = new RestClient("https://www.googleapis.com");
        client.Authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator(access_token);
        var request = new RestRequest("/oauth2/v1/userinfo", Method.GET);
        client.ExecuteAsync<Profile>(request, ProfileLoaded);
    }
    private void ProfileLoaded(IRestResponse<Profile> response)
    {
        Profile = response.Data;
    }

321291/profile.PNG

当访问令牌需要刷新时,存在类似的序列,但无需用户界面。

结论

我当然能从安全角度看到 OAuth2 的优势。应用程序在任何时候都不会拥有用户的凭据。整个交换过程发生在用户和 Google 之间。客户端不存储密码,应用程序也从不将密码传递给 Google API。

它确实使事情变得比简单的、向用户索取凭据并在每次调用中都包含它们的做法更复杂,但希望本文提供一个如何开始围绕 OAuth2 构建的示例。

历史

  • 2012年1月29日 - 首次上传
© . All rights reserved.