使用 Facebook、Google 和 Microsoft 账户在 WP8.0 应用 (MVVM) 中进行身份验证





0/5 (0投票)
本示例展示如何将 Windows Phone 8.0 应用连接到 Facebook、Google 和 Microsoft 账户。主要功能:登录/注销,以及一个包含反馈、社交网络分享、评论和电子邮件分享的关于页面。下载 C# (19.4 MB) 简介 … 继续阅读 →
本示例展示如何将 Windows Phone 8.0 应用连接到 Facebook、Google 和 Microsoft 账户。主要功能:登录/注销,以及一个包含反馈、社交网络分享、评论和电子邮件分享的关于页面。
下载 C# (19.4 MB)
引言
本示例展示如何将 Windows Phone 8.0 应用连接到 Facebook、Google 和 Microsoft 账户。
主要功能:登录/注销,以及一个包含反馈、社交网络分享、评论和电子邮件分享的关于页面。
构建示例
您只需要 Visual Studio 2012/Visual Studio 2013 和 Windows 8/Windows 8.1,均为 RTM 版本。
此示例需要安装 Live SDK (http://msdn.microsoft.com/en-US/onedrive/dn630256)。
描述
本示例展示如何将 Windows Phone 8.0 应用连接到 Facebook、Google 和 Microsoft 账户。
主要特点
- 登录/注销(对于注销,我添加了一些变通方法来修复 SDK 的提供商注销!)
- 关于页面,包含反馈、社交网络分享、评论和电子邮件分享(这里不重要,但包含在代码中)
注意:本示例使用了 MVVM Light 和 Cimbalino Windows Phone Toolkit。
本示例使用了
- Facebook SDK for Windows Phone (http://facebooksdk.net/docs/phone/)
- Google Apis Auth Client 和 Google Apis OAuth2 Client (https://nuget.net.cn/packages/Google.Apis.Auth/ 和 https://nuget.net.cn/packages/Google.Apis.Authentication/1.6.0-beta)
- Live SDK (http://msdn.microsoft.com/en-US/onedrive/dn630256)
对于每个提供商,都需要在其网站上获取应用 ID/客户端 ID/客户端密钥
对于 Google,请访问 https://console.developers.google.com/project 并创建一个新项目(APIs & auth > credentials)
对于 Facebook,请访问 https://developers.facebook.com/ 并创建一个新应用。
对于 Live SDK,请访问 https://account.live.com/developers/applications/index 并创建一个应用或使用现有应用
在开始之前,您应该修改 Constant 文件以添加客户端 ID / 客户端密钥 / 应用 ID,否则应用将失败!!
此文件位于 Resource 文件夹内。
/// <summary>
/// Defines the constants strings used in the app.
/// </summary>
public class Constants
{
/// <summary>
/// The facebook app id.
/// </summary>
public const string FacebookAppId = "<app id>";
/// <summary>
/// The google client identifier.
/// </summary>
public const string GoogleClientId = "<client id>";
/// <summary>
/// The google token file name.
/// </summary>
public const string GoogleTokenFileName = "Google.Apis.Auth.OAuth2.Responses.TokenResponse-user";
/// <summary>
/// The google client secret.
/// </summary>
public const string GoogleClientSecret = "<client secret>";
/// <summary>
/// The microsoft client identifier.
/// </summary>
public const string MicrosoftClientId = "<client id>";
...
}
现在,让我们看看如何连接到每个提供商。为了方便,我创建了一个 SessionService 来使用提供商值来管理登录和注销,这很好,因为在 LoginView 中,我将按钮设置为同一个命令,并且为每个命令设置了命令参数中的提供商。这使得 LoginView 和 LoginViewModel 更清晰、更简单。另一个好处是,例如,如果我需要连接到我的服务器来接受用户,我可以在身份验证后在 Session Manager 中完成,而无需将代码添加到每个提供商。
创建的类
- FacebookService 包含与 Facebook 账户身份验证相关的所有代码;
- MicrosoftService 包含与 Microsoft 账户身份验证相关的所有代码;
- GoogleService 包含与 Google 账户身份验证相关的所有代码;
- SessionService 调用所请求提供商的登录或注销方法;
FacebookService 是
/// <summary>
/// Defines the Facebook Service.
/// </summary>
public class FacebookService : IFacebookService
{
private readonly ILogManager _logManager;
private readonly FacebookSessionClient _facebookSessionClient;
/// <summary>
/// Initializes a new instance of the <see cref="FacebookService"/> class.
/// </summary>
/// <param name="logManager">
/// The log manager.
/// </param>
public FacebookService(ILogManager logManager)
{
_logManager = logManager;
_facebookSessionClient = new FacebookSessionClient(Constants.FacebookAppId);
}
/// <summary>
/// The login sync.
/// </summary>
/// <returns>
/// The <see cref="Task"/> object.
/// </returns>
public async Task<Session> LoginAsync()
{
Exception exception;
Session sessionToReturn = null;
try
{
var session = await _facebookSessionClient.LoginAsync("user_about_me,read_stream");
sessionToReturn = new Session
{
AccessToken = session.AccessToken,
Id = session.FacebookId,
ExpireDate = session.Expires,
Provider = Constants.FacebookProvider
};
return sessionToReturn;
}
catch (InvalidOperationException)
{
throw;
}
catch (Exception ex)
{
exception = ex;
}
await _logManager.LogAsync(exception);
return sessionToReturn;
}
/// <summary>
/// Logouts this instance.
/// </summary>
public async void Logout()
{
Exception exception = null;
try
{
_facebookSessionClient.Logout();
// clean all cookies from browser, is a workarround
await new WebBrowser().ClearCookiesAsync();
}
catch (Exception ex)
{
exception = ex;
}
if (exception != null)
{
await _logManager.LogAsync(exception);
}
}
}
注意:在注销时,我添加了一个变通方法来清除浏览器中的所有 cookie,否则在第一次登录时可以使用任何您想要的账户,但下次登录时将使用上次登录的账户。
GoogleService 是
/// <summary>
/// The google service.
/// </summary>
public class GoogleService : IGoogleService
{
private readonly ILogManager _logManager;
private readonly IStorageService _storageService;
private UserCredential _credential;
private Oauth2Service _authService;
private Userinfoplus _userinfoplus;
/// <summary>
/// Initializes a new instance of the <see cref="GoogleService" /> class.
/// </summary>
/// <param name="logManager">The log manager.</param>
/// <param name="storageService">The storage service.</param>
public GoogleService(ILogManager logManager, IStorageService storageService)
{
_logManager = logManager;
_storageService = storageService;
}
/// <summary>
/// The login async.
/// </summary>
/// <returns>
/// The <see cref="Task"/> object.
/// </returns>
public async Task<Session> LoginAsync()
{
Exception exception = null;
try
{
// Oauth2Service.Scope.UserinfoEmail
_credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = Constants.GoogleClientId,
ClientSecret = Constants.GoogleClientSecret
}, new[] { Oauth2Service.Scope.UserinfoProfile }, "user", CancellationToken.None);
var session = new Session
{
AccessToken = _credential.Token.AccessToken,
Provider = Constants.GoogleProvider,
ExpireDate =
_credential.Token.ExpiresInSeconds != null
? new DateTime(_credential.Token.ExpiresInSeconds.Value)
: DateTime.Now.AddYears(1),
Id = string.Empty
};
return session;
}
catch (TaskCanceledException taskCanceledException)
{
throw new InvalidOperationException("Login canceled.", taskCanceledException);
}
catch (Exception ex)
{
exception = ex;
}
await _logManager.LogAsync(exception);
return null;
}
/// <summary>
/// Gets the user information.
/// </summary>
/// <returns>
/// The user info.
/// </returns>
public async Task<Userinfoplus> GetUserInfo()
{
_authService = new Oauth2Service(new BaseClientService.Initializer()
{
HttpClientInitializer = _credential,
ApplicationName = AppResources.ApplicationTitle,
});
_userinfoplus = await _authService.Userinfo.V2.Me.Get().ExecuteAsync();
return _userinfoplus;
}
/// <summary>
/// The logout.
/// </summary>
public async void Logout()
{
await new WebBrowser().ClearCookiesAsync();
if (_storageService.FileExists(Constants.GoogleTokenFileName))
{
_storageService.DeleteFile(Constants.GoogleTokenFileName);
}
}
}
注意: 在 Google 提供商的注销操作中,没有注销方法,解决方案是清除所有 cookie 并删除登录操作中创建的文件。
MicrosoftService 是
/// <summary>
/// The microsoft service.
/// </summary>
public class MicrosoftService : IMicrosoftService
{
private readonly ILogManager _logManager;
private LiveAuthClient _authClient;
private LiveConnectSession _liveSession;
/// <summary>
/// Defines the scopes the application needs.
/// </summary>
private static readonly string[] Scopes = { "wl.signin", "wl.basic", "wl.offline_access" };
/// <summary>
/// Initializes a new instance of the <see cref="MicrosoftService"/> class.
/// </summary>
/// <param name="logManager">
/// The log manager.
/// </param>
public MicrosoftService(ILogManager logManager)
{
_logManager = logManager;
}
/// <summary>
/// The login async.
/// </summary>
/// <returns>
/// The <see cref="Task"/> object.
/// </returns>
public async Task<Session> LoginAsync()
{
Exception exception = null;
try
{
_authClient = new LiveAuthClient(Constants.MicrosoftClientId);
var loginResult = await _authClient.InitializeAsync(Scopes);
var result = await _authClient.LoginAsync(Scopes);
if (result.Status == LiveConnectSessionStatus.Connected)
{
_liveSession = loginResult.Session;
var session = new Session
{
AccessToken = result.Session.AccessToken,
ExpireDate = result.Session.Expires.DateTime,
Provider = Constants.MicrosoftProvider,
};
return session;
}
}
catch (LiveAuthException ex)
{
throw new InvalidOperationException("Login canceled.", ex);
}
catch (Exception e)
{
exception = e;
}
await _logManager.LogAsync(exception);
return null;
}
/// <summary>
/// The logout.
/// </summary>
public async void Logout()
{
if (_authClient == null)
{
_authClient = new LiveAuthClient(Constants.MicrosoftClientId);
var loginResult = await _authClient.InitializeAsync(Scopes);
}
_authClient.Logout();
}
}
SessionService 是
/// <summary>
/// The service session.
/// </summary>
public class SessionService : ISessionService
{
private readonly IApplicationSettingsService _applicationSettings;
private readonly IFacebookService _facebookService;
private readonly IMicrosoftService _microsoftService;
private readonly IGoogleService _googleService;
private readonly ILogManager _logManager;
/// <summary>
/// Initializes a new instance of the <see cref="SessionService" /> class.
/// </summary>
/// <param name="applicationSettings">The application settings.</param>
/// <param name="facebookService">The facebook service.</param>
/// <param name="microsoftService">The microsoft service.</param>
/// <param name="googleService">The google service.</param>
/// <param name="logManager">The log manager.</param>
public SessionService(IApplicationSettingsService applicationSettings,
IFacebookService facebookService,
IMicrosoftService microsoftService,
IGoogleService googleService, ILogManager logManager)
{
_applicationSettings = applicationSettings;
_facebookService = facebookService;
_microsoftService = microsoftService;
_googleService = googleService;
_logManager = logManager;
}
/// <summary>
/// Gets the session.
/// </summary>
/// <returns>The session object.</returns>
public Session GetSession()
{
var expiryValue = DateTime.MinValue;
string expiryTicks = LoadEncryptedSettingValue("session_expiredate");
if (!string.IsNullOrWhiteSpace(expiryTicks))
{
long expiryTicksValue;
if (long.TryParse(expiryTicks, out expiryTicksValue))
{
expiryValue = new DateTime(expiryTicksValue);
}
}
var session = new Session
{
AccessToken = LoadEncryptedSettingValue("session_token"),
Id = LoadEncryptedSettingValue("session_id"),
ExpireDate = expiryValue,
Provider = LoadEncryptedSettingValue("session_provider")
};
_applicationSettings.Set(Constants.LoginToken, true);
_applicationSettings.Save();
return session;
}
/// <summary>
/// The save session.
/// </summary>
/// <param name="session">
/// The session.
/// </param>
private void Save(Session session)
{
SaveEncryptedSettingValue("session_token", session.AccessToken);
SaveEncryptedSettingValue("session_id", session.Id);
SaveEncryptedSettingValue("session_expiredate", session.ExpireDate.Ticks.ToString(CultureInfo.InvariantCulture));
SaveEncryptedSettingValue("session_provider", session.Provider);
_applicationSettings.Set(Constants.LoginToken, true);
_applicationSettings.Save();
}
/// <summary>
/// The clean session.
/// </summary>
private void CleanSession()
{
_applicationSettings.Reset("session_token");
_applicationSettings.Reset("session_id");
_applicationSettings.Reset("session_expiredate");
_applicationSettings.Reset("session_provider");
_applicationSettings.Reset(Constants.LoginToken);
_applicationSettings.Save();
}
/// <summary>
/// The login async.
/// </summary>
/// <param name="provider">
/// The provider.
/// </param>
/// <returns>
/// The <see cref="Task"/> object.
/// </returns>
public async Task<bool> LoginAsync(string provider)
{
Exception exception = null;
try
{
Session session = null;
switch (provider)
{
case Constants.FacebookProvider:
session = await _facebookService.LoginAsync();
break;
case Constants.MicrosoftProvider:
session = await _microsoftService.LoginAsync();
break;
case Constants.GoogleProvider:
session = await _googleService.LoginAsync();
break;
}
if (session != null)
{
Save(session);
}
return true;
}
catch (InvalidOperationException e)
{
throw;
}
catch (Exception ex)
{
exception = ex;
}
await _logManager.LogAsync(exception);
return false;
}
/// <summary>
/// The logout.
/// </summary>
public async void Logout()
{
Exception exception = null;
try
{
var session = GetSession();
switch (session.Provider)
{
case Constants.FacebookProvider:
_facebookService.Logout();
break;
case Constants.MicrosoftProvider:
_microsoftService.Logout();
break;
case Constants.GoogleProvider:
_googleService.Logout();
break;
}
CleanSession();
}
catch (Exception ex)
{
exception = ex;
}
if (exception != null)
{
await _logManager.LogAsync(exception);
}
}
/// <summary>
/// Loads an encrypted setting value for a given key.
/// </summary>
/// <param name="key">
/// The key to load.
/// </param>
/// <returns>
/// The value of the key.
/// </returns>
private string LoadEncryptedSettingValue(string key)
{
string value = null;
var protectedBytes = _applicationSettings.Get<byte[]>(key);
if (protectedBytes != null)
{
byte[] valueBytes = ProtectedData.Unprotect(protectedBytes, null);
value = Encoding.UTF8.GetString(valueBytes, 0, valueBytes.Length);
}
return value;
}
/// <summary>
/// Saves a setting value against a given key, encrypted.
/// </summary>
/// <param name="key">
/// The key to save against.
/// </param>
/// <param name="value">
/// The value to save against.
/// </param>
/// <exception cref="System.ArgumentOutOfRangeException">
/// The key or value provided is unexpected.
/// </exception>
private void SaveEncryptedSettingValue(string key, string value)
{
if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(value))
{
byte[] valueBytes = Encoding.UTF8.GetBytes(value);
// Encrypt the value by using the Protect() method.
byte[] protectedBytes = ProtectedData.Protect(valueBytes, null);
_applicationSettings.Set(key, protectedBytes);
_applicationSettings.Save();
}
}
}
现在是时候构建用户界面了,由于我使用的是 MVVM,所以我创建了一个 LoginViewModel 来绑定到 LoginView。
LoginViewModel 是
/// <summary>
/// The login view model.
/// </summary>
public class LoginViewModel : ViewModelBase
{
private readonly ILogManager _logManager;
private readonly IMessageBoxService _messageBox;
private readonly INavigationService _navigationService;
private readonly ISessionService _sessionService;
private bool _inProgress;
/// <summary>
/// Initializes a new instance of the <see cref="LoginViewModel"/> class.
/// </summary>
/// <param name="navigationService">
/// The navigation service.
/// </param>
/// <param name="sessionService">
/// The session service.
/// </param>
/// <param name="messageBox">
/// The message box.
/// </param>
/// <param name="logManager">
/// The log manager.
/// </param>
public LoginViewModel(INavigationService navigationService,
ISessionService sessionService,
IMessageBoxService messageBox,
ILogManager logManager)
{
_navigationService = navigationService;
_sessionService = sessionService;
_messageBox = messageBox;
_logManager = logManager;
LoginCommand = new RelayCommand<string>(LoginAction);
}
/// <summary>
/// Gets or sets a value indicating whether in progress.
/// </summary>
/// <value>
/// The in progress.
/// </value>
public bool InProgress
{
get { return _inProgress; }
set { Set(() => InProgress, ref _inProgress, value); }
}
/// <summary>
/// Gets the facebook login command.
/// </summary>
/// <value>
/// The facebook login command.
/// </value>
public ICommand LoginCommand { get; private set; }
/// <summary>
/// Facebook's login action.
/// </summary>
/// <param name="provider">
/// The provider.
/// </param>
private async void LoginAction(string provider)
{
Exception exception = null;
bool isToShowMessage = false;
try
{
InProgress = true;
var auth = await _sessionService.LoginAsync(provider);
if (!auth)
{
await _messageBox.ShowAsync(AppResources.LoginView_LoginNotAllowed_Message,
AppResources.MessageBox_Title,
new List<string>
{
AppResources.Button_OK
});
}
else
{
_navigationService.NavigateTo(new Uri(Constants.MainView, UriKind.Relative));
}
InProgress = false;
}
catch (InvalidOperationException e)
{
InProgress = false;
isToShowMessage = true;
}
catch (Exception ex)
{
exception = ex;
}
if (isToShowMessage)
{
await _messageBox.ShowAsync(AppResources.LoginView_AuthFail, AppResources.ApplicationTitle, new List<string> { AppResources.Button_OK });
}
if (exception != null)
{
await _logManager.LogAsync(exception);
}
}
}
注意:在 LoginAction 中,参数 provider 是 LoginCommand 收到的 CommandParameter 的值,这在登录页面设置。
LoginPage.xaml 是
<phone:PhoneApplicationPage x:Class="AuthenticationSample.WP80.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP8"
xmlns:controls="clr-namespace:Facebook.Client.Controls;assembly=Facebook.Client"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:converters="clr-namespace:Cimbalino.Phone.Toolkit.Converters;assembly=Cimbalino.Phone.Toolkit"
Orientation="Portrait"
SupportedOrientations="Portrait"
shell:SystemTray.IsVisible="True"
mc:Ignorable="d">
<phone:PhoneApplicationPage.DataContext>
<Binding Mode="OneWay"
Path="LoginViewModel"
Source="{StaticResource Locator}" />
</phone:PhoneApplicationPage.DataContext>
<phone:PhoneApplicationPage.Resources>
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</phone:PhoneApplicationPage.Resources>
<phone:PhoneApplicationPage.FontFamily>
<StaticResource ResourceKey="PhoneFontFamilyNormal" />
</phone:PhoneApplicationPage.FontFamily>
<phone:PhoneApplicationPage.FontSize>
<StaticResource ResourceKey="PhoneFontSizeNormal" />
</phone:PhoneApplicationPage.FontSize>
<phone:PhoneApplicationPage.Foreground>
<StaticResource ResourceKey="PhoneForegroundBrush" />
</phone:PhoneApplicationPage.Foreground>
<!-- LayoutRoot is the root grid where all page content is placed -->
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- TitlePanel contains the name of the application and page title -->
<StackPanel x:Name="TitlePanel"
Grid.Row="0"
Margin="12,17,0,28">
<TextBlock Margin="12,0"
Style="{StaticResource PhoneTextNormalStyle}"
Text="{Binding LocalizedResources.ApplicationTitle,
Mode=OneWay,
Source={StaticResource LocalizedStrings}}" />
<TextBlock Margin="9,-7,0,0"
Style="{StaticResource PhoneTextTitle1Style}"
Text="{Binding LocalizedResources.LoginView_Title,
Mode=OneWay,
Source={StaticResource LocalizedStrings}}" />
</StackPanel>
<!-- ContentPanel - place additional content here -->
<Grid x:Name="ContentPanel"
Grid.Row="1"
Margin="24,0,0,-40">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Style="{StaticResource PhoneTextTitle2Style}"
Text="{Binding LocalizedResources.LoginView_UserAccount,
Mode=OneWay,
Source={StaticResource LocalizedStrings}}" />
<Button Grid.Row="1"
Margin="10"
Command="{Binding LoginCommand}"
CommandParameter="facebook"
Content="Facebook" />
<Button Grid.Row="2"
Margin="10"
Command="{Binding LoginCommand}"
CommandParameter="microsoft"
Content="Microsoft" />
<Button Grid.Row="3"
Margin="10"
Command="{Binding LoginCommand}"
CommandParameter="google"
Content="Google" />
</Grid>
<Grid Visibility="{Binding InProgress, Converter={StaticResource BooleanToVisibilityConverter}}"
Grid.Row="0"
Grid.RowSpan="2">
<Rectangle
Fill="Black"
Opacity="0.75" />
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding LocalizedResources.LoginView_AuthMessage,
Mode=OneWay,
Source={StaticResource LocalizedStrings}}" />
<ProgressBar IsIndeterminate="True" IsEnabled="True" Margin="0,60,0,0"/>
</Grid>
</Grid>
</phone:PhoneApplicationPage>
登录用户界面
源代码文件
- IFacebookService FacebookService 的接口
- IGoogleService GoogleService 的接口
- ILogManager LogManager 的接口
- IMicrosoftService MicrosoftService 的接口
- ISessionProvider 所有提供商接口的接口(通用方法)
- ISessionService SessionService 的接口
- Session 类用于保存有关会话的信息(提供商、令牌、过期日期)
- FacebookService 类,包含使用 Facebook 提供商登录/注销的所有逻辑
- GoogleService 类,包含使用 Google 提供商登录/注销的所有逻辑
- MicrosoftService 类,包含使用 Microsoft 提供商登录/注销的所有逻辑
- SessionService 类,用于管理登录/注销(它将使用前面描述的所有提供商服务)
- LoginViewModel 类,用于绑定到 LoginView.xaml
- LoginView 类,代表登录页面
- MainViewModel 类,用于绑定到 MainView.xaml
- MainView 类,在登录成功后显示
- AboutViewModel 类,用于绑定到 AboutView.xaml
- AboutView 类,代表关于页面
- ViewModelLocator 类包含对应用程序中所有视图模型的静态引用,并提供绑定的入口点。
构建示例
- 启动 Visual Studio Express 2012 for Windows 8,然后选择 **文件** > **打开** > **项目/解决方案**。
- 转到您解压缩示例的目录。进入以示例命名的目录,然后双击 Visual Studio Express 2012 for Windows 8 Solution (.sln) 文件。
- 按 F7 或使用 **生成** > **生成解决方案** 来构建示例。
注意:您可以在 Windows 8.1 中使用 Visual Studio 2013。
运行示例
要调试并运行应用程序,请按 F5 或使用 **调试** > **开始调试**。要在不调试的情况下运行应用程序,请按 Ctrl+F5 或使用 **调试** > **不调试运行**。
相关示例
更多信息
在 Twitter 上提问 @saramgsilva