教程:使用外部租户在 .NET MAUI shell 应用中登录用户

本教程是系列教程的最后一部分,演示如何创建 .NET 多平台应用 UI (.NET MAUI) shell 应用并使用 Microsoft Entra 管理中心准备进行身份验证。 在本系列教程的第 2 部分中,你添加了自定义 Microsoft 身份验证库 (MSAL) 客户端帮助程序,以初始化 MSAL SDK、安装所需的库并包括图像资源。 此最后一步演示了如何在 .NET MAUI 中添加登录和注销代码,以及如何在 Android 平台上运行 shell 应用。

在本教程中,你将:

  • 添加登录和注销代码。
  • 修改应用 Shell。
  • 添加特定于平台的代码。
  • 添加应用设置。
  • 运行并测试 .NET MAUI shell 应用。

先决条件

添加登录和注销代码

.NET MAUI 应用的用户界面 (UI) 由映射到每个目标平台的本机控件的对象构成。 用于创建 .NET MAUI 应用 UI 的主控件组包括页面、布局和视图。

添加主视图页

后续步骤将对代码进行组织,以便定义 main view

  1. 从不再需要 MainPage.xaml 和 MainPage.xaml.cs 的项目中删除这两者。 在“解决方案资源管理器”窗格中,找到 MainPage.xaml 的条目,右键单击它并选择“删除”。

  2. 右键单击“SignInMaui”项目,然后选择“新增”>“文件夹”。 将文件夹命名为 Views。

  3. 右键单击“视图”。

  4. 选择“添加”>“新项...”。

  5. 在模板列表中选择“.NET MAUI”。

  6. 选择“.NET MAUI ContentPage (XAML)”模板。 将文件命名为 MainView.xaml。

  7. 选择 添加

  8. MainView.xaml 文件将在新的文档选项卡中打开,显示表示页面 UI 的所有 XAML 标记。 将 XAML 标记替换为以下标记:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="SignInMaui.Views.MainView"
                 Title="Microsoft Entra External ID"
                 >
        <Shell.BackButtonBehavior>
            <BackButtonBehavior IsVisible="False" IsEnabled="False" />
        </Shell.BackButtonBehavior>
    
        <ScrollView>
            <VerticalStackLayout 
                Spacing="25" 
                Padding="30,0" 
                VerticalOptions="Center">
    
                <Image
                    Source="external_id.png"
                    SemanticProperties.Description="External ID"
                    HeightRequest="200"
                    HorizontalOptions="Center" />
    
                <Label 
                    Text="CIAM"
                    SemanticProperties.HeadingLevel="Level1"
                    FontSize="26"
                    HorizontalOptions="Center" />
    
                <Label 
                    Text="MAUI sample"
                    SemanticProperties.HeadingLevel="Level1"
                    FontSize="26"
                    HorizontalOptions="Center" />
    
                <Button 
                    x:Name="SignInButton"
                    Text="Sign In"
                    SemanticProperties.Hint="Sign In"
                    Clicked="OnSignInClicked"
                    HorizontalOptions="Center"
                    IsEnabled="False"/>
    
            </VerticalStackLayout>
        </ScrollView>
     
    </ContentPage>
    
  9. 保存文件。

    让我们将页面上 XAML 控件的关键部分细分一下:

    • <ContentPage> 是 MainView 类的根对象。
    • <VerticalStackLayout> 是 ContentPage 的子对象。 此布局控件将其子控件逐一垂直排列。
    • <Image> 会显示一张图像,在本例中,它使用你之前下载的 azureactive_directory.png_。
    • <Label> 控件显示文本。
    • 用户可按下 <Button> 来引发 Clicked 事件。 可运行代码来响应 Clicked 事件。
    • Clicked="OnSignInClicked":按钮的 Clicked 事件分配给 OnSignInClicked 事件处理程序,将在代码隐藏文件中定义该处理程序。 将在下一步骤中创建此代码。

处理 OnSignInClicked 事件

下一步是为按钮的 Clicked 事件添加代码。

  1. 在 Visual Studio 的“解决方案资源管理器”窗格中,展开 MainView.xaml 文件以显示其代码隐藏文件 MainView.xaml.cs。 打开 MainView.xaml.cs,并将文件的内容替换为以下代码:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License.
    
    using SignInMaui.MSALClient;
    using Microsoft.Identity.Client;
    
    namespace SignInMaui.Views
    {
        public partial class MainView : ContentPage
        {
            public MainView()
            {
                InitializeComponent();
    
                IAccount cachedUserAccount = PublicClientSingleton.Instance.MSALClientHelper.FetchSignedInUserFromCache().Result;
    
                _ = Dispatcher.DispatchAsync(async () =>
                {
                    if (cachedUserAccount == null)
                    {
                        SignInButton.IsEnabled = true;
                    }
                    else
                    {
                        await Shell.Current.GoToAsync("claimsview");
                    }
                });
            }
    
            private async void OnSignInClicked(object sender, EventArgs e)
            {
                await PublicClientSingleton.Instance.AcquireTokenSilentAsync();
                await Shell.Current.GoToAsync("claimsview");
            }
            protected override bool OnBackButtonPressed() { return true; }
    
        }
    }
    

    MainView 类是一个内容页面,负责显示应用的主视图。 在构造函数中,它使用 PublicClientSingleton 实例中的 MSALClientHelper 来检索缓存的用户帐户,如果未找到缓存的用户帐户,则启用“登录”按钮。

    单击“登录”按钮时,它会调用 AcquireTokenSilentAsync 方法来以无提示方式获取令牌,并使用 Shell.Current.GoToAsync 方法导航到 claimsview 页面。 此外,会重写 OnBackButtonPressed 方法以返回 true,表示已为此视图禁用后退按钮。

添加声明视图页

后续步骤将对代码进行组织,以便定义 ClaimsView 页面。 该页面将显示在 ID 令牌中找到的用户声明。

  1. 在 Visual Studio 的“解决方案资源管理器”窗格中,右键单击“视图”。

  2. 选择“添加”>“新项...”。

  3. 在模板列表中选择“.NET MAUI”。

  4. 选择“.NET MAUI ContentPage (XAML)”模板。 将文件命名为 ClaimsView.xaml。

  5. 选择 添加

  6. ClaimsView.xaml 文件将在新的文档选项卡中打开,显示表示页面 UI 的所有 XAML 标记。 将 XAML 标记替换为以下标记:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="SignInMaui.Views.ClaimsView"
                 Title="ID Token View">
        <Shell.BackButtonBehavior>
            <BackButtonBehavior IsVisible="False" IsEnabled="False" />
        </Shell.BackButtonBehavior>
        <VerticalStackLayout>
            <Label 
                Text="CIAM"
                FontSize="26"
                HorizontalOptions="Center" />
            <Label 
                Text="MAUI sample"
                FontSize="26"
                Padding="0,0,0,20"
                HorizontalOptions="Center" />
    
            <Label 
                Padding="0,20,0,0"
                VerticalOptions="Center" 
                HorizontalOptions="Center"
                FontSize="18"
                Text="Claims found in ID token"
                />
            <ListView ItemsSource="{Binding IdTokenClaims}"
                      x:Name="Claims">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <Grid Padding="0, 0, 0, 0">
                                <Label Grid.Column="1" 
                                       Text="{Binding}" 
                                       HorizontalOptions="Center" />
                            </Grid>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
            <Button
                x:Name="SignOutButton"
                Text="Sign Out"
                HorizontalOptions="Center"
                Clicked="SignOutButton_Clicked" />
        </VerticalStackLayout>
    </ContentPage>
    

    此 XAML 标记代码表示 .NET MAUI 应用中声明视图的 UI 布局。 它首先使用标题定义 ContentPage 并禁用后退按钮行为。

    VerticalStackLayout 中,有几个 Label 元素显示静态文本,后跟一个名为 ClaimsListView,它绑定到名为 IdTokenClaims 的集合以显示在 ID 令牌中找到的声明。 每个声明都使用 DataTemplateViewCell 内呈现,并在网格中居中显示为 Label

    最后,布局底部中间有一个 Sign Out 按钮,单击时会触发 SignOutButton_Clicked 事件处理程序。

处理 ClaimsView 数据

下一步是添加代码来处理 ClaimsView 数据。

  1. 在 Visual Studio 的“解决方案资源管理器”窗格中,展开 ClaimsView.xaml 文件以显示其代码隐藏文件 ClaimsView.xaml.cs。 打开 ClaimsView.xaml.cs,并将文件的内容替换为以下代码:

    using SignInMaui.MSALClient;
    using Microsoft.Identity.Client;
    
    namespace SignInMaui.Views;
    
    public partial class ClaimsView : ContentPage
    {
        public IEnumerable<string> IdTokenClaims { get; set; } = new string[] {"No claims found in ID token"};
        public ClaimsView()
        {
            BindingContext = this;
            InitializeComponent();
    
            _ = SetViewDataAsync();
        }
    
        private async Task SetViewDataAsync()
        {
            try
            {
                _ = await PublicClientSingleton.Instance.AcquireTokenSilentAsync();
    
                IdTokenClaims = PublicClientSingleton.Instance.MSALClientHelper.AuthResult.ClaimsPrincipal.Claims.Select(c => c.Value);
    
                Claims.ItemsSource = IdTokenClaims;
            }
    
            catch (MsalUiRequiredException)
            {
                await Shell.Current.GoToAsync("claimsview");
            }
        }
    
        protected override bool OnBackButtonPressed() { return true; }
    
        private async void SignOutButton_Clicked(object sender, EventArgs e)
        {
            await PublicClientSingleton.Instance.SignOutAsync().ContinueWith((t) =>
            {
                return Task.CompletedTask;
            });
    
            await Shell.Current.GoToAsync("mainview");
        }
    }
    

    ClaimsView.xaml.cs 代码表示 .NET MAUI 应用中声明视图的代码隐藏。 它首先导入必要的命名空间,并定义用于扩展 ContentPageClaimsView 类。 IdTokenClaims 属性是可枚举的字符串,最初设置为单个字符串,指示找不到声明。

    ClaimsView 构造函数将绑定上下文设置为当前实例,初始化视图组件,并异步调用 SetViewDataAsync 方法。 SetViewDataAsync 方法尝试以无提示方式获取令牌,从身份验证结果中检索声明,并将 IdTokenClaims 属性设置为在名为 ClaimsListView 中显示这些声明。 如果发生 MsalUiRequiredException,指示身份验证需要用户交互,则应用将导航到声明视图。

    OnBackButtonPressed 方法会重写后退按钮行为以始终返回 true,从而防止用户从此视图中返回。 SignOutButton_Clicked 事件处理程序使用 PublicClientSingleton 实例将用户注销,完成后导航到 main view

修改应用 Shell

AppShell 类定义应用的视觉层次结构,即用于创建应用 UI 的 XAML 标记。 更新 AppShell,使其了解 Views

  1. 在“解决方案资源管理器”窗格中双击 AppShell.xaml 文件来打开 XAML 编辑器。 将 XAML 标记替换为以下代码:

    <?xml version="1.0" encoding="UTF-8" ?>
    <Shell
        x:Class="SignInMaui.AppShell"
        xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:local="clr-namespace:SignInMaui.Views"
        Shell.FlyoutBehavior="Disabled">
    
        <ShellContent
            Title="Home"
            ContentTemplate="{DataTemplate local:MainView}"
            Route="MainPage" />
    </Shell>
    

    XAML 代码定义一个 AppShell 类,该类禁用浮出控件行为,并将主内容设置为标题为 HomeShellContent 元素和指向 MainView 类的内容模板。

  2. 在 Visual Studio 的“解决方案资源管理器”窗格中,展开 AppShell.xaml 文件以显示其代码隐藏文件 AppShell.xaml.cs。 打开 AppShell.xaml.cs,并将文件的内容替换为以下代码:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License.
    using SignInMaui.Views;
    
    namespace SignInMaui;
    
    public partial class AppShell : Shell
    {
        public AppShell()
        {
            InitializeComponent();
            Routing.RegisterRoute("mainview", typeof(MainView));
            Routing.RegisterRoute("claimsview", typeof(ClaimsView));
        }
    }
    

    更新 AppShell.xaml.cs 文件,使其包含 MainViewClaimsView 的必要路由注册。 通过调用 InitializeComponent() 方法,可确保 AppShell 类的初始化。 RegisterRoute() 方法将 mainviewclaimsview 路由与其各自的视图类型(MainViewClaimsView)相关联。

添加特定于平台的代码

.NET MAUI 应用项目包含一个 Platforms 文件夹,其中每个子文件夹表示一个 .NET MAUI 可以面向的平台。 若要提供特定于 Android 应用程序的行为来对默认应用程序类进行补充,请执行以下步骤:

  1. 在“解决方案资源管理器”窗格中双击 Platforms/Android/AndroidManifest.xml 文件来打开 XML 编辑器。 更新以下属性:

    • 将应用程序名称设置为 MAUI CIAM。
    • 将包名称设置为 SignInMaui.Droid。
    • 将最低 Android 版本设置为“Android 5.0 (API 级别 21)”。
  2. 在“解决方案资源管理器”窗格中双击 Platforms/Android/MainActivity.cs 文件来打开 csharp 编辑器。 将文件内容替换为以下代码:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License.
    using Android.App;
    using Android.Content;
    using Android.Content.PM;
    using Android.OS;
    using SignInMaui.MSALClient;
    using Microsoft.Identity.Client;
    
    namespace SignInMaui;
    
    [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
    public class MainActivity : MauiAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            // configure platform specific params
            PlatformConfig.Instance.RedirectUri = $"msal{PublicClientSingleton.Instance.MSALClientHelper.AzureAdConfig.ClientId}://auth";
            PlatformConfig.Instance.ParentWindow = this;
    
            // Initialize MSAL and platformConfig is set
            _ = Task.Run(async () => await PublicClientSingleton.Instance.MSALClientHelper.InitializePublicClientAppAsync()).Result;
        }
    
        protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
        {
            base.OnActivityResult(requestCode, resultCode, data);
            AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data);
        }
    }
    

    让我们将已添加的代码的关键部分细分一下:

    • 必需的 using 语句包含在顶部。
    • MainActivity 类已定义,它继承自 MauiAppCompatActivity,而后者是 .NET MAUI 中 Android 平台的基类。
    • [Activity] 属性应用于 MainActivity 类,为 Android 活动指定各种设置。
      • Theme = "@style/Maui.SplashTheme" 设置活动的初始主题。
      • MainLauncher = true 将此活动指定为应用程序的主要入口点。
      • ConfigurationChanges 指定活动可处理的配置更改,例如屏幕大小、方向、UI 模式、屏幕布局、最小屏幕大小和密度。
    • OnCreate 方法会被重写,以在创建活动时提供自定义逻辑。
      • base.OnCreate(savedInstanceState) 调用方法的基实现。
      • PlatformConfig.Instance.RedirectUri 设置为基于 PublicClientSingleton.Instance.MSALClientHelper.AzureAdConfig.ClientId 动态生成的值。 它配置 MSAL 客户端的重定向 URI。
      • PlatformConfig.Instance.ParentWindow 设置为当前活动实例,该实例指定身份验证相关操作的父窗口。
      • PublicClientSingleton.Instance.MSALClientHelper.InitializePublicClientAppAsync() 使用名为 MSALClientHelper 的单一实例中的帮助程序方法异步初始化 MSAL 客户端应用。 Task.Run 用于在后台线程上执行初始化,.Result 用于同步等待任务完成。
    • OnActivityResult 方法会被重写,以处理当前活动启动的活动的结果。
      • base.OnActivityResult(requestCode, resultCode, data) 调用方法的基实现。
      • AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data) 根据收到的请求代码、结果代码和意向数据设置身份验证延续事件参数。 这用于在外部活动返回结果后继续执行身份验证流。
  3. 在 Visual Studio 的“解决方案资源管理器”窗格中,选择“平台”。

  4. 右键单击 Android 文件夹 >“添加”>“新项...”。

  5. 选择“C# 项”>“类”。 命名文件 MsalActivity.cs

  6. MsalActivity.cs 文件的内容替换为以下代码:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License.
    
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    using Android.App;
    using Android.Content;
    using Android.OS;
    using Android.Runtime;
    using Android.Views;
    using Android.Widget;
    using Microsoft.Identity.Client;
    
    namespace MauiAppBasic.Platforms.Android.Resources
    {
        [Activity(Exported =true)]
        [IntentFilter(new[] { Intent.ActionView },
            Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault },
            DataHost = "auth",
            DataScheme = "msalEnter_the_Application_Id_Here")]
        public class MsalActivity : BrowserTabActivity
        {
        }
    }
    

    让我们将已添加的代码的关键部分细分一下:

    • MsalActivity 类是在 MauiAppBasic.Platforms.Android.Resources 命名空间中声明的。 该类继承自 BrowserTabActivity 类,表示它扩展了其功能。
    • 该类使用 [Activity(Exported = true)] 属性进行修饰,该属性表示活动已导出且可通过其他方法访问。
    • 意向筛选器使用“[IntentFilter(...)]”属性进行指定。 它将活动配置为截获 ActionView 意向。
    • 意向筛选器设置为使用指定的 DataScheme (msalEnter_the_Application_Id_Here) 和 DataHost (auth) 处理 ActionView 意向。 此配置允许活动通过截获和处理 ActionView 意向来处理身份验证过程。 将 Enter_the_Application_Id_Here 替换为之前注册的应用的应用程序(客户端)ID。

添加应用设置

设置允许将配置应用行为的数据与代码分离,从而允许在不重新生成应用的情况下更改行为。 MauiAppBuilder 提供 ConfigurationManager 以在 .NET MAUI 应用中配置设置。 让我们将 appsettings.json 文件添加为 EmbeddedResource

若要创建 appsettings.json,请执行以下步骤:

  1. 在 Visual Studio 的“解决方案资源管理器”窗格中,右键单击“SignInMaui”项目 >“添加”>“新项...”。

  2. 选择“Web”>“JavaScript JSON 配置文件”。 命名文件 appsettings.json

  3. 选择 添加

  4. 选择 appsettings.json

  5. 在“属性”窗格中,将“生成操作”设置为“嵌入的资源”。

  6. 在“属性”窗格中,将“复制到输出目录”设置为“始终复制”。

  7. appsettings.json 文件的内容替换为以下代码:

    {
      "AzureAd": {
        "Authority": "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/",
        "ClientId": "Enter_the_Application_Id_Here",
        "CacheFileName": "msal_cache.txt",
        "CacheDir": "C:/temp"
      },
      "DownstreamApi": {
        "Scopes": "openid offline_access"
      }
    }
    
  8. appsettings.json 中,找到占位符:

    1. 查找 Enter_the_Tenant_Subdomain_Here 并将其替换为目录(租户)子域。 例如,如果租户主域为 contoso.onmicrosoft.com,请使用 contoso。 如果没有租户名称,请了解如何读取租户详细信息
    2. Enter_the_Application_Id_Here,并将其替换为之前注册的应用的应用程序(客户端)ID。

使用自定义 URL 域(可选)

使用自定义域可完全标记身份验证 URL。 从用户的角度来看,用户在身份验证过程中仍留在你的域中,而不是重定向到 ciamlogin.com 域名

按照以下步骤使用自定义域:

  1. 使用为外部租户中的应用启用自定义 URL 域中的步骤为外部租户启用自定义 URL 域。

  2. 打开 appsettings.json 文件

    1. 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,请了解如何读取租户详细信息
    2. 添加值为 [Enter_the_Custom_Domain_Here] 的 knownAuthorities 属性

对 appsettings.json 文件进行更改后,如果自定义 URL 域为 login.contoso.com 且租户 ID 为 aaaabbbb-0000-cccc-1111-dddd2222eeee,则文件应类似于以下代码片段

{
  "AzureAd": {
    "Authority": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
    "ClientId": "Enter_the_Application_Id_Here",
    "CacheFileName": "msal_cache.txt",
    "CacheDir": "C:/temp",
    "KnownAuthorities": ["login.contoso.com"]
  },
  "DownstreamApi": {
    "Scopes": "openid offline_access"
  }
}

运行并测试 .NET MAUI 移动应用

根据设计,.NET MAUI 应用可在多个操作系统和设备上运行。 你需要选择要用于测试和调试应用的目标。

Visual Studio 工具栏中,将“调试目标”设置为要用于调试和测试的设备。 以下步骤演示如何将调试目标设置为 Android:

  1. 选择“调试目标”下拉列表。
  2. 选择“Android Emulator”。
  3. 选择仿真器设备。

按 F5 运行应用,或者选择 Visual Studio 顶部的播放按钮。

  1. 现在可以测试示例 .NET MAUI Android 应用。 运行应用后,Android 应用窗口将显示在模拟器中:

    Android 应用程序中“登录”按钮的屏幕截图。

  2. 在显示的 Android 窗口中,选择“登录”按钮。 随即将打开一个浏览器窗口,提示你登录。

    Android 应用程序中提示输入凭据的用户提示屏幕截图。

    在登录过程中,系统会提示你授予各种权限(以允许应用程序访问数据)。 成功登录并同意后,应用程序屏幕会显示主页。

    登录后 Android 应用程序中主页的屏幕截图。

另请参阅