教程:从 Blazor WebAssembly 应用让用户登录并调用受保护的 APITutorial: Sign in users and call a protected API from a Blazor WebAssembly app

在本教程中,你将使用 Microsoft 标识平台并在 Azure Active Directory (Azure AD) 中注册应用,从而构建 Blazor WebAssembly 应用,用户可登录该应用并通过 Microsoft Graph 检索数据。In this tutorial, you build a Blazor WebAssembly app that signs in users and gets data from Microsoft Graph by using the Microsoft identity platform and registering your app in Azure Active Directory (Azure AD).

在本教程中:In this tutorial:

  • 创建新的 Blazor WebAssembly 应用,并将其配置为通过 Microsoft 标识平台使用 Azure Active Directory (Azure AD) 进行身份验证和授权Create a new Blazor WebAssembly app configured to use Azure Active Directory (Azure AD) for authentication and authorization using the Microsoft identity platform
  • 从受保护的 Web API(在本例中为 Microsoft Graph)中检索数据Retrieve data from a protected web API, in this case Microsoft Graph

本教程使用 .NET Core 3.1。This tutorial uses .NET Core 3.1. .NET 文档包含有关如何使用 ASP.NET Core 5.0 保护 Blazor WebAssembly 应用的说明。The .NET docs contain instructions on how to secure a Blazor WebAssembly app using ASP.NET Core 5.0.

我们还提供了关于 Blazor Server 的教程We also have a tutorial for Blazor Server.

先决条件Prerequisites

在 Azure 门户中注册应用Register the app in the Azure portal

使用 Azure Active Directory (Azure AD) 进行身份验证的每个应用都必须注册到 Azure AD。Every app that uses Azure Active Directory (Azure AD) for authentication must be registered with Azure AD. 按照注册应用程序中的说明及以下规范进行操作:Follow the instructions in Register an application with these specifications:

  • 对于“支持的帐户类型”设置,请选择“仅限此组织目录中的帐户”。 For Supported account types, select Accounts in this organizational directory only.
  • 将“重定向 URI”下拉框的设置保留为“Web”并输入 https://localhost:5001/authentication/login-callbackLeave the Redirect URI drop down set to Web and enter https://localhost:5001/authentication/login-callback. 在 Kestrel 上运行的应用的默认端口为 5001。The default port for an app running on Kestrel is 5001. 如果应用通过一个不同的端口提供,请指定该端口号而非 5001If the app is available on a different port, specify that port number instead of 5001.

注册后,在“管理”下,选择“身份验证” > “隐式授权和混合流” 。Once registered, under Manage, select Authentication > Implicit grant and hybrid flows. 选择“访问令牌”和“ID 令牌”,然后选择“保存” 。Select Access tokens and ID tokens, and then select Save.

使用 .NET Core CLI 创建应用Create the app using the .NET Core CLI

若要创建应用,需要使用最新的 Blazor 模板。To create the app you need the latest Blazor templates. 可以通过以下命令为 .NET Core CLI 安装这些模板:You can install them for the .NET Core CLI with the following command:

dotnet new --install Microsoft.AspNetCore.Components.WebAssembly.Templates::3.2.1

然后,运行以下命令来创建应用程序。Then run the following command to create the application. 将命令中的占位符替换为你的应用的概览页面中的正确信息,然后在命令行界面中执行该命令。Replace the placeholders in the command with the proper information from your app's overview page and execute the command in a command shell. 使用 -o|--output 选项指定的输出位置将创建一个项目文件夹(如果该文件夹不存在)并成为应用程序名称的一部分。The output location specified with the -o|--output option creates a project folder if it doesn't exist and becomes part of the app's name.

dotnet new blazorwasm2 --auth SingleOrg --calls-graph -o {APP NAME} --client-id "{CLIENT ID}" --tenant-id "{TENANT ID}"
占位符Placeholder Azure 门户中的名称Azure portal name 示例Example
{APP NAME} BlazorWASMSample
{CLIENT ID} 应用程序(客户端)IDApplication (client) ID 41451fa7-0000-0000-0000-69eff5a761fd
{TENANT ID} 目录(租户)IDDirectory (tenant) ID e86c78e2-0000-0000-0000-918e0565a45e

测试应用Test the app

现在可以生成并运行应用了。You can now build and run the app. 运行此模板应用时,必须使用 --framework 指定要运行的框架。When you run this template app, you must specify the framework to run using --framework. 本教程使用 .NET Standard 2.1,但该模板也支持其他框架。This tutorial uses the .NET Standard 2.1, but the template supports other frameworks as well.

dotnet run --framework netstandard2.1

在浏览器中导航到 https://localhost:5001,使用 Azure AD 用户帐户登录,以查看通过 Microsoft 标识平台运行并让用户登录的应用。In your browser, navigate to https://localhost:5001, and log in using an Azure AD user account to see the app running and logging users in with the Microsoft identity platform.

本主题的 ASP.NET 文档中说明了此模板的组件,这些组件允许通过 Microsoft 标识平台使用 Azure AD 进行登录。The components of this template that enable logins with Azure AD using the Microsoft identity platform are explained in the ASP.NET doc on this topic.

通过受保护的 API (Microsoft Graph) 检索数据Retrieving data from a protected API (Microsoft Graph)

Microsoft Graph 包含使用户可访问 Microsoft 365 数据的 API,并且支持 Microsoft 标识平台颁发的令牌,这使得它成为很棒的受保护 API,可用作示例。Microsoft Graph contains APIs that provide access to Microsoft 365 data for your users, and it supports the tokens issued by the Microsoft identity platform, which makes it a good protected API to use as an example. 在本部分,你将添加代码来调用 Microsoft Graph,并在应用程序的“提取数据”页面上显示用户的电子邮件。In this section, you add code to call Microsoft Graph and display the user's emails on the application's "Fetch data" page.

本部分采用常见方法编写,该方法使用命名客户端调用受保护的 API。This section is written using a common approach to calling a protected API using a named client. 这一方法可用于其他要调用的受保护 API。The same method can be used for other protected APIs you want to call. 但是,如果你确实计划从应用程序调用 Microsoft Graph,那么可使用 Graph SDK 来减少样板。However, if you do plan to call Microsoft Graph from your application you can use the Graph SDK to reduce boilerplate. .NET 文档包含有关如何使用 Graph SDK 的说明。The .NET docs contain instructions on how to use the Graph SDK.

在开始之前,请注销你的应用,因为你将对所需权限进行更改,并且你的当前令牌将不起作用。Before you start, log out of your app since you'll be making changes to the required permissions, and your current token won't work. 如果你尚未这样做,请再次运行应用,并在更新以下代码之前选择“注销”。If you haven't already, run your app again and select Log out before updating the code below.

现在,你将更新应用的注册和代码,以拉取用户的电子邮件并在应用中显示这些消息。Now you will update your app's registration and code to pull a user's emails and display the messages within the app.

首先,将 Mail.Read API 权限添加到应用的注册,使 Azure AD 知道该应用将请求访问其用户的电子邮件。First, add the Mail.Read API permission to the app's registration so that Azure AD is aware that the app will request to access its users' email.

  1. 在 Azure 门户的“应用注册”中选择你的应用。In the Azure portal, select your app in App registrations.
  2. 在“管理”下选择“API 权限”。Under Manage, select API permissions.
  3. 选择“添加权限” > “Microsoft Graph” 。Select Add a permission > Microsoft Graph.
  4. 选择“委托的权限”,然后搜索并选择“Mail.Read”权限。Select Delegated Permissions, then search for and select the Mail.Read permission.
  5. 选择“添加权限”。Select Add permissions.

接下来,将以下项添加到 netstandard2.1“ItemGroup”中项目的 .csproj 文件中。Next, add the following to your project's .csproj file in the netstandard2.1 ItemGroup. 这使你可以在下一步中创建自定义 HttpClient。This will allow you to create the custom HttpClient in the next step.

<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />

然后修改后面几个步骤中指定的代码。Then modify the code as specified in the next few steps. 这些更改会将访问令牌添加到发送至 Microsoft Graph API 的传出请求中。These changes will add access tokens to the outgoing requests sent to the Microsoft Graph API. ASP.NET Core Blazor WebAssembly 其他安全方案中更加详细地讨论了此模式。This pattern is discussed in more detail in ASP.NET Core Blazor WebAssembly additional security scenarios.

首先,使用以下代码创建名为 GraphAPIAuthorizationMessageHandler.cs 的新文件。First, create a new file named GraphAPIAuthorizationMessageHandler.cs with the following code. 该处理程序用于将 User.ReadMail.Read 作用域的访问令牌添加到发送至 Microsoft Graph API 的传出请求中。This handler will be user to add an access token for the User.Read and Mail.Read scopes to outgoing requests to the Microsoft Graph API.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class GraphAPIAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public GraphAPIAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://microsoftgraph.chinacloudapi.cn" },
            scopes: new[] { "https://microsoftgraph.chinacloudapi.cn/User.Read", "https://microsoftgraph.chinacloudapi.cn/Mail.Read" });
    }
}

然后,将 Program.cs 中 Main 方法的内容替换为以下代码。Then, replace the contents of the Main method in Program.cs with the following code. 该代码利用新的 GraphAPIAuthorizationMessageHandler,并将 User.ReadMail.Read 添加为用户首次登录时应用将请求的默认作用域。This code makes use of the new GraphAPIAuthorizationMessageHandler and adds User.Read and Mail.Read as default scopes the app will request when the user first signs in.

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");

builder.Services.AddScoped<GraphAPIAuthorizationMessageHandler>();

builder.Services.AddHttpClient("GraphAPI",
        client => client.BaseAddress = new Uri("https://microsoftgraph.chinacloudapi.cn"))
    .AddHttpMessageHandler<GraphAPIAuthorizationMessageHandler>();

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("User.Read");
    options.ProviderOptions.DefaultAccessTokenScopes.Add("Mail.Read");
});

await builder.Build().RunAsync();

最后,将 FetchData.razor 页的内容替换为以下代码。Finally, replace the contents of the FetchData.razor page with the following code. 此代码从 Microsoft Graph API 提取用户电子邮件数据,并将其显示为列表。This code fetches user email data from the Microsoft Graph API and displays them as a list. OnInitializedAsync 中,创建使用正确的访问令牌的新 HttpClient,并将其用于向 Microsoft Graph API 发出请求。In OnInitializedAsync, the new HttpClient that uses the proper access token is created and used to make the request to the Microsoft Graph API.

@page "/fetchdata"
@using System.ComponentModel.DataAnnotations
@using System.Text.Json.Serialization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.Extensions.Logging
@inject IAccessTokenProvider TokenProvider
@inject IHttpClientFactory ClientFactory
@inject IHttpClientFactory HttpClientFactory

<p>This component demonstrates fetching data from a service.</p>

@if (messages == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <h1>Hello @userDisplayName !!!!</h1>
    <table class="table">
        <thead>
            <tr>
                <th>Subject</th>
                <th>Sender</th>
                <th>Received Time</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var mail in messages)
            {
                <tr>
                    <td>@mail.Subject</td>
                    <td>@mail.Sender</td>
                    <td>@mail.ReceivedTime</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {

    private string userDisplayName;
    private List<MailMessage> messages = new List<MailMessage>();

    private HttpClient _httpClient;

    protected override async Task OnInitializedAsync()
    {
        _httpClient = HttpClientFactory.CreateClient("GraphAPI");
        try {
            var dataRequest = await _httpClient.GetAsync("https://microsoftgraph.chinacloudapi.cn/beta/me");

            if (dataRequest.IsSuccessStatusCode)
            {
                var userData = System.Text.Json.JsonDocument.Parse(await dataRequest.Content.ReadAsStreamAsync());
                userDisplayName = userData.RootElement.GetProperty("displayName").GetString();
            }

            var mailRequest = await _httpClient.GetAsync("https://microsoftgraph.chinacloudapi.cn/beta/me/messages?$select=subject,receivedDateTime,sender&$top=10");

            if (mailRequest.IsSuccessStatusCode)
            {
                var mailData = System.Text.Json.JsonDocument.Parse(await mailRequest.Content.ReadAsStreamAsync());
                var messagesArray = mailData.RootElement.GetProperty("value").EnumerateArray();

                foreach (var m in messagesArray)
                {
                    var message = new MailMessage();
                    message.Subject = m.GetProperty("subject").GetString();
                    message.Sender = m.GetProperty("sender").GetProperty("emailAddress").GetProperty("address").GetString();
                    message.ReceivedTime = m.GetProperty("receivedDateTime").GetDateTime();
                    messages.Add(message);
                }
            }
        }
        catch (AccessTokenNotAvailableException ex)
        {
            // Tokens are not valid - redirect the user to log in again
            ex.Redirect();
        }
    }

    public class MailMessage
    {
        public string Subject;
        public string Sender;
        public DateTime ReceivedTime;
    }
}

现在重新启动应用。Now launch the app again. 你会注意到,系统将提示你向应用提供访问权限以阅读你的电子邮件。You'll notice that you're prompted to give the app access to read your mail. 当应用请求 Mail.Read 作用域时,会出现这种情况。This is expected when an app requests the Mail.Read scope.

在授权同意后,导航到“提取数据”页来阅读某封电子邮件。After granting consent, navigate to the "Fetch data" page to read some email.

最终应用的屏幕截图。它有一个显示为“Hello Nicholas”的标题,它显示了属于 Nicholas 的电子邮件列表。

后续步骤Next steps