适用于: 员工租户
外部租户(了解详细信息)
本教程演示如何生成 Windows 演示文稿窗体(WPF)桌面应用,并准备使用 Microsoft Entra 管理中心进行身份验证。
在本教程中,你将:
- 配置 WPF 桌面应用以使用其应用注册详细信息。
- 构建可以登录用户并代表用户获取令牌的桌面应用。
先决条件
- 在 Microsoft Entra 管理中心注册新应用,为 任何组织目录中的帐户和个人 Microsoft 帐户配置。 有关更多详细信息 ,请参阅注册应用程序 。 在应用程序 概述 页中记录以下值供以后使用:
- 应用程序(客户端)ID
- 目录(租户)ID
- 目录(租户)域名(例如 contoso.onmicrosoft.com 或 contoso.com)。
- 使用 移动和桌面应用程序 平台配置添加以下重定向 URI。 有关更多详细信息 ,请参阅如何在应用程序中添加重定向 URI 。
- 重定向 URI:
https://login.microsoftonline.com/common/oauth2/nativeclient
- 重定向 URI:
- 将应用与 Microsoft Entra 管理中心中的用户流相关联。 此用户流可以跨多个应用程序使用。 有关详细信息,请参阅 为外部租户中的应用创建自助服务注册用户流 ,并将 应用程序添加到用户流。
- .NET 7.0 SDK 或更高版本。
- 本教程使用了 Visual Studio Code,但可以使用任何支持 React 应用程序的集成开发环境 (IDE)。
创建 WPF 桌面应用程序
打开终端并导航到希望项目所在的文件夹。
初始化 WPF 桌面应用并导航到其根文件夹。
dotnet new wpf --language "C#" --name sign-in-dotnet-wpf cd sign-in-dotnet-wpf
安装软件包
安装配置提供程序,帮助我们的应用从应用设置文件中的键值对读取配置数据。 使用这些配置抽象可将配置值绑定到 .NET 对象的实例。
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder
安装 Microsoft 身份验证库 (MSAL),其中包含获取令牌所需的所有关键组件。 还可以安装 MSAL 代理库,以处理与桌面身份验证代理的交互。
dotnet add package Microsoft.Identity.Client
dotnet add package Microsoft.Identity.Client.Broker
创建 appsettings.json 文件,并添加注册配置
在应用的根文件夹中创建 appsettings.json 文件。
将应用注册详细信息添加到 appsettings.json 文件中。
{ "AzureAd": { "Authority": "https://<Enter_the_Tenant_Subdomain_Here>.ciamlogin.com/", "ClientId": "<Enter_the_Application_Id_Here>" } }
- 将
Enter_the_Tenant_Subdomain_Here
替换为目录(租户)子域。 - 将
Enter_the_Application_Id_Here
替换为之前注册的应用的应用程序(客户端)ID。
- 将
创建应用设置文件后,我们将创建另一个名为 AzureAdConfig.cs 的文件,它将帮助你从应用设置文件读取配置。 在应用的根文件夹中创建 AzureAdConfig.cs 文件。
在 AzureAdConfig.js 文件中,定义
ClientId
和Authority
属性的获取器和设置器。 添加以下代码:namespace sign_in_dotnet_wpf { public class AzureAdConfig { public string Authority { get; set; } public string ClientId { get; set; } } }
使用自定义 URL 域(可选)
使用自定义域对身份验证 URL 进行完全品牌化。 从用户的角度来看,用户在身份验证过程中仍然停留在您的域名上,而不会被重定向到 ciamlogin.com 域名。
按照以下步骤使用自定义域:
使用启用外部租户中应用的自定义 URL 域名中的步骤,为您的外部租户启用自定义 URL 域名。
打开 appsettings.json 文件:
- 将属性的值
Authority
更新为 https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here. 将Enter_the_Custom_Domain_Here
替换为您的自定义 URL 域,并将Enter_the_Tenant_ID_Here
替换为您的租户 ID。 如果没有租户 ID,请了解如何读取租户详细信息。 - 添加
knownAuthorities
属性,并使用值 [Enter_the_Custom_Domain_Here]。
- 将属性的值
对 appsettings.json 文件进行更改后,如果自定义 URL 域 login.contoso.com,并且租户 ID 为 aaaabb-0000-cccc-1111-dd222eeee,则文件应类似于以下代码片段:
{
"AzureAd": {
"Authority": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
"ClientId": "Enter_the_Application_Id_Here",
"KnownAuthorities": ["login.contoso.com"]
}
}
修改项目文件
导航到应用根文件夹中的 sign-in-dotnet-wpf.csproj 文件。
在此文件中,执行以下两个步骤:
- 修改 sign-in-dotnet-wpf.csproj 文件以指示应用在编译项目时将 appsettings.json 文件复制到输出目录。 将以下代码片段添加到 sign-in-dotnet-wpf.csproj 文件:
- 将目标框架设置为面向 windows10.0.19041.0 版本,以帮助从令牌缓存读取缓存的令牌,如令牌缓存帮助程序类中所示。
<Project Sdk="Microsoft.NET.Sdk"> ... <!-- Set target framework to target windows10.0.19041.0 build --> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework> <!-- target framework --> <RootNamespace>sign_in_dotnet_wpf</RootNamespace> <Nullable>enable</Nullable> <UseWPF>true</UseWPF> </PropertyGroup> <!-- Copy appsettings.json file to output folder. --> <ItemGroup> <None Remove="appsettings.json" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="appsettings.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </EmbeddedResource> </ItemGroup> </Project>
创建令牌缓存辅助类
创建用于初始化令牌缓存的令牌缓存帮助程序类。 应用程序在尝试获取新令牌之前,将会尝试从缓存中读取令牌。 如果在缓存中找不到令牌,则应用程序将获取新令牌。 注销后,将会清除缓存中的所有帐户和所有相应的访问令牌。
在应用的根文件夹中创建 TokenCacheHelper.cs 文件。
打开 TokenCacheHelper.cs 文件。 将包和命名空间添加到文件。 在以下步骤中,将会通过将相关逻辑添加到
TokenCacheHelper
类来使用代码逻辑填充此文件。using System.IO; using System.Security.Cryptography; using Microsoft.Identity.Client; namespace sign_in_dotnet_wpf { static class TokenCacheHelper{} }
将构造函数添加到定义缓存文件路径的
TokenCacheHelper
类。 对于打包的桌面应用(MSIX 包,也称为桌面网桥),执行程序集文件夹是只读的。 在这种情况下,我们需要使用Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path + "\msalcache.bin"
,它是打包应用的按应用读/写文件夹。namespace sign_in_dotnet_wpf { static class TokenCacheHelper { static TokenCacheHelper() { try { CacheFilePath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path, ".msalcache.bin3"); } catch (System.InvalidOperationException) { CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin3"; } } public static string CacheFilePath { get; private set; } private static readonly object FileLock = new object(); } }
请添加代码来处理令牌缓存序列化。
ITokenCache
接口可实现对缓存操作的公共访问。ITokenCache
接口包含用于订阅缓存序列化事件的方法,而接口ITokenCacheSerializer
则会公开需要在缓存序列化事件中使用的方法,以对缓存进行序列化/反序列化。TokenCacheNotificationArgs
包含访问缓存时由Microsoft.Identity.Client
(MSAL) 调用所使用的参数。ITokenCacheSerializer
接口在TokenCacheNotificationArgs
回调中可用。将以下代码添加到
TokenCacheHelper
类:static class TokenCacheHelper { static TokenCacheHelper() {...} public static string CacheFilePath { get; private set; } private static readonly object FileLock = new object(); public static void BeforeAccessNotification(TokenCacheNotificationArgs args) { lock (FileLock) { args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath) ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath), null, DataProtectionScope.CurrentUser) : null); } } public static void AfterAccessNotification(TokenCacheNotificationArgs args) { if (args.HasStateChanged) { lock (FileLock) { File.WriteAllBytes(CacheFilePath, ProtectedData.Protect(args.TokenCache.SerializeMsalV3(), null, DataProtectionScope.CurrentUser) ); } } } } internal static void EnableSerialization(ITokenCache tokenCache) { tokenCache.SetBeforeAccess(BeforeAccessNotification); tokenCache.SetAfterAccess(AfterAccessNotification); }
在
BeforeAccessNotification
方法中,可从文件系统读取缓存,如果缓存不为空,则将其反序列化并加载。 在AfterAccessNotification
(MSAL) 访问缓存后,将会调用Microsoft.Identity.Client
方法。 如果缓存已更改,则对其进行序列化,并将更改保存到缓存中。EnableSerialization
包含ITokenCache.SetBeforeAccess()
和ITokenCache.SetAfterAccess()
方法:ITokenCache.SetBeforeAccess()
可将委托设置为在任何库方法访问缓存之前收到通知。 通过此操作,代理可以选择反序列化在TokenCacheNotificationArgs
中指定的应用程序和帐户的缓存条目。ITokenCache.SetAfterAccess()
可将委托设置为在任何库方法访问缓存之后收到通知。 通过此操作,委托可以选择序列化在TokenCacheNotificationArgs
中指定的应用程序和帐户的缓存条目。
创建 WPF 桌面应用 UI
修改 MainWindow.xaml 文件以添加应用的 UI 元素。 打开应用根文件夹中的 MainWindow.xaml 文件,并添加带有 <Grid></Grid>
控件部分的以下代码段。
<StackPanel Background="Azure">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="SignInButton" Content="Sign-In" HorizontalAlignment="Right" Padding="5" Click="SignInButton_Click" Margin="5" FontFamily="Segoe Ui"/>
<Button x:Name="SignOutButton" Content="Sign-Out" HorizontalAlignment="Right" Padding="5" Click="SignOutButton_Click" Margin="5" Visibility="Collapsed" FontFamily="Segoe Ui"/>
</StackPanel>
<Label Content="Authentication Result" Margin="0,0,0,-5" FontFamily="Segoe Ui" />
<TextBox x:Name="ResultText" TextWrapping="Wrap" MinHeight="120" Margin="5" FontFamily="Segoe Ui"/>
<Label Content="Token Info" Margin="0,0,0,-5" FontFamily="Segoe Ui" />
<TextBox x:Name="TokenInfoText" TextWrapping="Wrap" MinHeight="70" Margin="5" FontFamily="Segoe Ui"/>
</StackPanel>
此代码会添加关键 UI 元素。 处理 UI 元素功能的方法和对象在 MainWindow.xaml.cs 文件中定义,我们将在下一步中创建该文件。
- 用于登录用户的按钮。 当用户选择此按钮后,将会调用
SignInButton_Click
方法。 - 用于注销用户的按钮。 当用户选择此按钮后,将会调用
SignOutButton_Click
方法。 - 一个文本框,用于在用户尝试登录后显示身份验证结果详细信息。 此处显示的信息由
ResultText
对象返回。 - 一个文本框,用于在用户成功登录后显示令牌详细信息。 此处显示的信息由
TokenInfoText
对象返回。
将代码添加到 MainWindow.xaml.cs 文件
MainWindow.xaml.cs 文件包含为 MainWindow.xaml 文件中 UI 元素的行为提供运行时逻辑的代码。
打开应用根文件夹中的 MainWindow.xaml.cs 文件。
在文件中添加以下代码以导入包,并为我们创建的方法定义占位符。
using Microsoft.Identity.Client; using System; using System.Linq; using System.Windows; using System.Windows.Interop; namespace sign_in_dotnet_wpf { public partial class MainWindow : Window { string[] scopes = new string[] { }; public MainWindow() { InitializeComponent(); } private async void SignInButton_Click(object sender, RoutedEventArgs e){...} private async void SignOutButton_Click(object sender, RoutedEventArgs e){...} private void DisplayBasicTokenInfo(AuthenticationResult authResult){...} } }
将以下代码添加到
SignInButton_Click
方法中。 当用户选择“登录”按钮时,将会调用此方法。private async void SignInButton_Click(object sender, RoutedEventArgs e) { AuthenticationResult authResult = null; var app = App.PublicClientApp; ResultText.Text = string.Empty; TokenInfoText.Text = string.Empty; IAccount firstAccount; var accounts = await app.GetAccountsAsync(); firstAccount = accounts.FirstOrDefault(); try { authResult = await app.AcquireTokenSilent(scopes, firstAccount) .ExecuteAsync(); } catch (MsalUiRequiredException ex) { try { authResult = await app.AcquireTokenInteractive(scopes) .WithAccount(firstAccount) .WithParentActivityOrWindow(new WindowInteropHelper(this).Handle) .WithPrompt(Prompt.SelectAccount) .ExecuteAsync(); } catch (MsalException msalex) { ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}"; } catch (Exception ex) { ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}"; return; } if (authResult != null) { ResultText.Text = "Sign in was successful."; DisplayBasicTokenInfo(authResult); this.SignInButton.Visibility = Visibility.Collapsed; this.SignOutButton.Visibility = Visibility.Visible; } } }
GetAccountsAsync()
返回用户令牌缓存中该应用程序的所有可用帐户。IAccount
接口表示单个帐户的相关信息。为了获取令牌,应用会尝试使用
AcquireTokenSilent
方法以无提示方式获取令牌,以验证缓存中是否存在可接受的令牌。 例如,方法AcquireTokenSilent
可能会失败,因为用户已注销。当 MSAL 检测到问题可以通过需要交互式作来解决时,它会引发异常MsalUiRequiredException
。 此异常会导致应用以交互方式获取令牌。调用
AcquireTokenInteractive
方法将出现提示用户进行登录的窗口。 当用户首次需要进行身份验证时,应用通常会要求用户进行交互式登录。 在执行用户获取令牌的无提示操作时,他们也可能需要登录。 首次执行AcquireTokenInteractive
后,AcquireTokenSilent
会成为获取令牌的常用方法将以下代码添加到
SignOutButton_Click
方法中。 当用户选择“退出登录”按钮时,将会调用此方法。private async void SignOutButton_Click(object sender, RoutedEventArgs e) { var accounts = await App.PublicClientApp.GetAccountsAsync(); if (accounts.Any()) { try { await App.PublicClientApp.RemoveAsync(accounts.FirstOrDefault()); this.ResultText.Text = "User has signed-out"; this.TokenInfoText.Text = string.Empty; this.SignInButton.Visibility = Visibility.Visible; this.SignOutButton.Visibility = Visibility.Collapsed; } catch (MsalException ex) { ResultText.Text = $"Error signing-out user: {ex.Message}"; } } }
方法
SignOutButton_Click
会清除缓存中的所有帐户和所有相应的访问令牌。 用户下次尝试登录时,将必须以交互方式登录。将以下代码添加到
DisplayBasicTokenInfo
方法中。 此方法可显示有关令牌的基本信息。private void DisplayBasicTokenInfo(AuthenticationResult authResult) { TokenInfoText.Text = ""; if (authResult != null) { TokenInfoText.Text += $"Username: {authResult.Account.Username}" + Environment.NewLine; TokenInfoText.Text += $"{authResult.Account.HomeAccountId}" + Environment.NewLine; } }
将代码添加到 App.xaml.cs 文件
App.xaml 是你声明应用中所使用资源的位置。 它是应用的入口点。 App.xaml.cs 是 App.xaml 的后台代码文件。 App.xaml.cs 还定义了应用程序的启动窗口。
打开应用根文件夹中的 App.xaml.cs 文件,然后将以下代码添加到该文件中。
using System.Windows;
using System.Reflection;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
namespace sign_in_dotnet_wpf
{
public partial class App : Application
{
static App()
{
CreateApplication();
}
public static void CreateApplication()
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("sign_in_dotnet_wpf.appsettings.json");
AppConfiguration = new ConfigurationBuilder()
.AddJsonStream(stream)
.Build();
AzureAdConfig azureADConfig = AppConfiguration.GetSection("AzureAd").Get<AzureAdConfig>();
var builder = PublicClientApplicationBuilder.Create(azureADConfig.ClientId)
.WithAuthority(azureADConfig.Authority)
.WithDefaultRedirectUri();
_clientApp = builder.Build();
TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);
}
private static IPublicClientApplication _clientApp;
private static IConfiguration AppConfiguration;
public static IPublicClientApplication PublicClientApp { get { return _clientApp; } }
}
}
在此步骤中,将会加载 appsettings.json 文件。 配置生成器可帮助读取 appsettings.json 文件中定义的应用配置。 由于 WPF 应用属于桌面应用,因此还需将它定义为公共客户端应用。 方法 TokenCacheHelper.EnableSerialization
会启用令牌缓存序列化。
运行应用
运行应用并登录以测试应用程序
在终端中,导航到 WPF 应用的根文件夹,并通过在终端中运行命令
dotnet run
来运行应用。启动示例后,应会看到一个带有“登录”按钮的窗口。 选择该“登录”按钮。
在登录页上,输入帐户电子邮件地址。 如果没有帐户,请选择“无帐户? 创建一个”,以启动注册流。 按照此流程创建新帐户并登录。
登录后,你将看到一个屏幕,显示登录成功以及存储在检索到的令牌中的用户帐户的基本信息。 基本信息显示在登录屏幕的 “令牌信息 ”部分中