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

适用于:带灰色 X 号的白色圆圈。 员工租户 带白色对号的绿色圆圈。 外部租户(了解详细信息

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

在本教程中,你将:

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

先决条件

添加登录和注销代码

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

添加主视图页

后续步骤将组织我们的代码,以定义 main view

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

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

  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 类是一个内容页面,负责显示应用的主视图。 在构造函数中,它使用MSALClientHelperPublicClientSingleton实例检索缓存的用户帐户,并启用登录按钮(如果未找到缓存的用户帐户)。

    单击登录按钮时,它会调用AcquireTokenSilentAsync该方法以无提示方式获取令牌,并使用claimsview该方法导航到Shell.Current.GoToAsync页面。 此外,会重写 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 元素显示静态文本,后跟一个名为 ListViewClaims,它绑定到名为 IdTokenClaims 的集合以显示在 ID 令牌中找到的声明。 每个声明都使用 ViewCellDataTemplate 内呈现,并在网格中居中显示为 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 应用中声明视图的后置代码。 它首先导入必要的命名空间并定义 ClaimsView 扩展的 ContentPage类。 IdTokenClaims 属性是可枚举的字符串,最初设置为单个字符串,指示找不到声明。

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

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

修改应用程序界面

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 类,该类禁用浮出控件行为,并将主内容设置为一个 ShellContent 元素,具有标题 Home,并通过内容模板指向 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 路由与其各自的视图类型相关联, MainView 以及 ClaimsView

添加特定于平台的代码

.NET MAUI 应用项目包含平台文件夹,每个子文件夹表示 .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 意向。
    • 意向筛选器设置为使用指定的 ActionView (DataScheme) 和 msalEnter_the_Application_Id_Here ("auth") 处理 DataHost 意向。 此配置允许活动通过截获和处理 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 并将其替换为 Directory (tenant) 子域。 例如,如果租户主域名是 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. 添加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",
    "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 应用程序中主页的屏幕截图。

另请参阅