将 Graph API 和 ASP.NET Core Blazor WebAssembly 结合使用

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

本文介绍如何在 Blazor WebAssembly 应用中使用 Microsoft Graph,使应用能够访问 Microsoft Cloud 资源。

介绍了两种方法:

  • Graph SDKMicrosoft Graph SDK简化了生成用于访问 Microsoft Graph 的高质量、高效且可复原的应用的过程。 选择本文顶部的“Graph SDK”按钮可以采用此方法。

  • 使用图形 API 的已命名 HttpClient已命名的 HttpClient可以将 Microsoft Graph API 请求直接发送到 Microsoft Graph。 选择本文顶部的“使用图形 API 的已命名 HttpClient”按钮可以采用此方法。

本文中的指南并非要替代其他 Microsoft 文档集中的 Microsoft Graph 文档和 Azure 安全指南。 在生产环境中实现 Microsoft Graph 之前,请评估本文的其他资源部分的安全指南。 遵循 Microsoft 的最佳做法来限制应用的攻击面。

使用 Microsoft Graph 和 Blazor WebAssembly 的其他方法由以下 Microsoft Graph 和 Azure 示例提供:

要提供有关上述两个示例中任一示例的反馈,请在示例的 GitHub 存储库上提出问题。 如果要针对 Azure 示例提出问题,请在打开的注释中提供示例的链接,因为 Azure 示例存储库 (Azure-Samples) 包含许多示例。 详细描述问题,并根据需要包含示例代码。 将最小的应用放入 GitHub 中,以重现问题或错误。 将 Azure 帐户配置数据提交到公共存储库之前,请务必从示例中将其删除。

要就本文或 ASP.NET Core 提供反馈或寻求帮助,请参阅 ASP.NET Core Blazor 基础知识

重要

本文中所述的方案适用于将 Microsoft Entra (ME-ID) 用作 identity 提供者,而不是 AAD B2C。 目前不支持将 Microsoft Graph 与客户端 Blazor WebAssembly 应用和 AAD B2C identity 提供程序一起使用,因为该应用需要客户端密码,这无法在客户端 Blazor 应用中受到保护。 对于使用图形 API 的 AAD B2C 独立 Blazor WebAssembly 应用,请创建一个后端服务器 (Web) API,以代表用户访问图形 API。 客户端应用对用户进行身份验证并授权用户调用 Web API 以安全地访问 Microsoft Graph 并将数据从基于服务器的 Web API 返回到客户端 Blazor 应用。 客户端密码在基于服务器的 Web API 中安全地维护,而不是在客户端上的 Blazor 应用中进行维护。 切勿在客户端 Blazor 应用中存储客户端密码。

支持使用托管的 Blazor WebAssembly 应用,其中 Server 应用使用 Graph SDK/API,通过 Web API 向 Client 应用提供 Graph 数据。 有关详细信息,请参阅本文的托管的 Blazor WebAssembly 解决方案部分。

本文中的示例会利用新的 .NET/C# 功能。 使用 .NET 7 或更早版本中的示例时,需要稍作修改。 但是,与 Microsoft Graph 交互有关的文本和代码示例对所有版本的 ASP.NET Core 都是相同的。

以下指南适用于 Microsoft Graph v5。

用于 Blazor 应用的 Microsoft Graph SDK 被称为 Microsoft Graph .NET 客户端库。

Graph SDK 示例需要独立 Blazor WebAssembly 应用中的以下包引用。 如果应用已启用 MSAL 身份验证,则已引用了前两个包,例如,在按照使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用中的指导创建应用时。

Graph SDK 示例需要独立 Blazor WebAssembly 应用或托管 Blazor WebAssembly 解决方案的 Client 应用中的以下包引用。 如果应用已启用 MSAL 身份验证,则已引用了前两个包,例如,在按照使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用中的指导创建应用时。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

在 Azure 门户中,授予对应用应能够代表用户访问的 Microsoft Graph 数据的委托访问权限(范围)†。 对于本文中的示例,应用注册应包括读取用户数据的委托权限(API 权限中的 Microsoft.Graph>User.Read 范围,类型:委派)。 通过 User.Read 范围,用户能够登录到应用,并且应用可以读取已登录用户的个人资料和公司信息。 有关详细信息,请参阅 Microsoft identity 平台中的权限和同意概述以及 Microsoft Graph 权限概述

†权限和范围是同一个意思,在安全文档和 Azure 门户中可以互换使用。 除非文本引用 Azure 门户,否则本文在引用 Graph 权限时使用作用域/作用域

范围不区分大小写,因此 User.Readuser.read 相同。 请随意使用任一格式,但建议在应用程序代码中选择一致的格式。

在 Azure 门户中将 Microsoft Graph API 范围添加到应用注册后,将以下应用设置配置添加到应用中的 wwwroot/appsettings.json 文件,其中包括具有 Microsoft Graph 版本和范围的 Graph 基 URL。 在下面的示例中,指定了 User.Read 范围以用于本文后面部分的示例。 范围不区分大小写。

"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}

在前面的示例中,{VERSION} 占位符是 Microsoft Graph API 的版本(例如:v1.0)。

下面是将 ME-ID 用作其 identity 提供者的应用的完整 wwwroot/appsettings.json 配置文件示例,其中为 Microsoft Graph 指定了读取用户数据(user.read 范围):

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}

在前面的示例中,{TENANT ID} 占位符是目录(租户)ID,{CLIENT ID} 占位符是应用程序(客户端)ID。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

将以下 GraphClientExtensions 类添加到独立应用。 向 AuthenticateRequestAsync 方法中 AccessTokenRequestOptionsScopes 属性提供范围。

将以下 GraphClientExtensions 类添加到托管的 Blazor WebAssembly解决方案的独立应用或 Client 应用。 向 AuthenticateRequestAsync 方法中 AccessTokenRequestOptionsScopes 属性提供范围。

如果没有获取访问令牌,以下代码不会为 Graph 请求设置持有者授权标头。

GraphClientExtensions.cs:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;
using IAccessTokenProvider = 
    Microsoft.AspNetCore.Components.WebAssembly.Authentication.IAccessTokenProvider;

namespace BlazorSample;

internal static class GraphClientExtensions
{
    public static IServiceCollection AddGraphClient(
            this IServiceCollection services, string? baseUrl, List<string>? scopes)
    {
        if (string.IsNullOrEmpty(baseUrl) || scopes?.Count == 0)
        {
            return services;
        }

        services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
            options =>
            {
                scopes?.ForEach((scope) =>
                {
                    options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
                });
            });

        services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();

        services.AddScoped(sp =>
        {
            return new GraphServiceClient(
                new HttpClient(),
                sp.GetRequiredService<IAuthenticationProvider>(),
                baseUrl);
        });

        return services;
    }

    private class GraphAuthenticationProvider(IAccessTokenProvider tokenProvider, 
        IConfiguration config) : IAuthenticationProvider
    {
        private readonly IConfiguration config = config;

        public IAccessTokenProvider TokenProvider { get; } = tokenProvider;

        public async Task AuthenticateRequestAsync(RequestInformation request, 
            Dictionary<string, object>? additionalAuthenticationContext = null, 
            CancellationToken cancellationToken = default)
        {
            var result = await TokenProvider.RequestAccessToken(
                new AccessTokenRequestOptions()
                {
                    Scopes = 
                        config.GetSection("MicrosoftGraph:Scopes").Get<string[]>() ??
                        [ "user.read" ]
                });

            if (result.TryGetToken(out var token))
            {
                request.Headers.Add("Authorization", 
                    $"{CoreConstants.Headers.Bearer} {token.Value}");
            }
        }
    }
}

重要

有关上述代码为何使用 DefaultAccessTokenScopes 而不是使用 AdditionalScopesToConsent 添加范围的说明,请参阅 DefaultAccessTokenScopesAdditionalScopesToConsent 部分。

Program 文件中,使用 AddGraphClient 扩展方法添加 Graph 客户端服务和配置。 如果未在应用设置文件中找到这些设置,则以下代码默认为版本 1.0 Microsoft Graph 基址和 User.Read 作用域:

var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

使用 Graph SDK 从组件调用 Graph API

以下 UserData 组件使用注入的 GraphServiceClient 来获取用户的 ME-ID 个人资料数据并显示其手机号码。

对于你在 ME-ID 中创建的任何测试用户,请确保在 Azure 门户中为用户的 ME-ID 个人资料提供一个手机号码。

UserData.razor:

@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
    <p>Mobile Phone: @user.MobilePhone</p>
}

@code {
    private Microsoft.Graph.Models.User? user;

    protected override async Task OnInitializedAsync()
    {
        user = await Client.Me.GetAsync();
    }
}

NavMenu 组件(Layout/NavMenu.razor)中添加指向组件页面的链接:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>

提示

要将用户添加到应用,请参阅“使用或不使用应用角色将用户分配到应用注册”部分。

在本地使用 Graph SDK 进行测试时,建议为每个测试使用一个新的专用/隐身浏览器会话,防止 Cookie 干扰测试的情况继续存在。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

使用 Graph SDK 自定义用户声明

在下面的示例中,应用基于用户 ME-ID 个人资料数据为用户创建手机号码和办公地点声明。 应用必须在 ME-ID 中配置User.Read Graph API 范围。 此方案的任何测试用户都必须在其 ME-ID 个人资料中包含手机号码和办公地点,可以通过 Azure 门户添加。

在以下自定义用户帐户工厂中:

  • 为了方便起见,我们包含了一个 ILogger (logger) ,便于你要在 CreateUserAsync 方法中记录信息或错误时使用。
  • 如果引发了 AccessTokenNotAvailableException,则会将用户重定向到 identity 提供程序以登录到其帐户。 如果请求访问令牌失败,可以采取其他不同的操作。 例如,应用可以记录 AccessTokenNotAvailableException 并创建支持票证以进一步调查。
  • 框架的 RemoteUserAccount 表示用户的帐户。 如果应用需要扩展 RemoteUserAccount 的自定义用户帐户类,请将下面代码中你的自定义用户帐户类替换为 RemoteUserAccount

CustomAccountFactory.cs:

using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor,
        IServiceProvider serviceProvider, ILogger<CustomAccountFactory> logger,
        IConfiguration config) 
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IServiceProvider serviceProvider = serviceProvider;
    private readonly string? baseUrl = string.Join("/",
        config.GetSection("MicrosoftGraph")["BaseUrl"] ?? 
            "https://graph.microsoft.com",
        config.GetSection("MicrosoftGraph")["Version"] ??
            "v1.0");

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity is not null &&
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null && !string.IsNullOrEmpty(baseUrl))
            {
                try
                {
                    var client = new GraphServiceClient(
                        new HttpClient(),
                        serviceProvider
                            .GetRequiredService<IAuthenticationProvider>(),
                        baseUrl);

                    var user = await client.Me.GetAsync();

                    if (user is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            user.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            user.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}

将 MSAL 身份验证配置为使用自定义用户帐户工厂。

确认 Program 文件使用 Microsoft.AspNetCore.Components.WebAssembly.Authentication 命名空间:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

本部分中的示例基于通过 wwwroot/appsettings.json 文件中的 MicrosoftGraph 节从应用配置中读取具有版本和范围的基 URL 的方法。 按照本文前面的指南,Program 文件中应已存在以下行:

var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

Program 文件中,找到对 AddMsalAuthentication 扩展方法的调用。 将代码更新为以下内容,其中包括对 AddAccountClaimsPrincipalFactory 的调用,该调用使用 CustomAccountFactory 添加帐户声明主体工厂。

如果应用使用扩展 RemoteUserAccount 的自定义用户帐户类,请使用以下代码将自定义用户帐户类替换为 RemoteUserAccount

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount,
        CustomAccountFactory>();

在用户使用 ME-ID 进行身份验证后,你可以使用以下UserClaims组件来研究用户的声明:

UserClaims.razor:

@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}
else
{
    <p>No claims found.</p>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}

NavMenu 组件(Layout/NavMenu.razor)中添加指向组件页面的链接:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>

在本地使用 Graph SDK 进行测试时,建议为每个测试使用一个新的专用/隐身浏览器会话,防止 Cookie 干扰测试的情况继续存在。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

以下指南适用于 Microsoft Graph v4。 如果要将应用从 SDK v4 升级到 v5,请参阅 Microsoft Graph .NET SDK v5 更改日志和升级指南

用于 Blazor 应用的 Microsoft Graph SDK 被称为 Microsoft Graph .NET 客户端库。

Graph SDK 示例需要独立 Blazor WebAssembly 应用中的以下包引用。 如果应用已启用 MSAL 身份验证,则已引用了前两个包,例如,在按照使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用中的指导创建应用时。

Graph SDK 示例需要独立 Blazor WebAssembly 应用或托管 Blazor WebAssembly 解决方案的 Client 应用中的以下包引用。 如果应用已启用 MSAL 身份验证,则已引用了前两个包,例如,在按照使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用中的指导创建应用时。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

在 Azure 门户中,授予对应用应能够代表用户访问的 Microsoft Graph 数据的委托访问权限(范围)†。 对于本文中的示例,应用注册应包括读取用户数据的委托权限(API 权限中的 Microsoft.Graph>User.Read 范围,类型:委派)。 通过 User.Read 范围,用户能够登录到应用,并且应用可以读取已登录用户的个人资料和公司信息。 有关详细信息,请参阅 Microsoft identity 平台中的权限和同意概述以及 Microsoft Graph 权限概述

†权限和范围是同一个意思,在安全文档和 Azure 门户中可以互换使用。 除非文本引用 Azure 门户,否则本文在引用 Graph 权限时使用作用域/作用域

范围不区分大小写,因此 User.Readuser.read 相同。 请随意使用任一格式,但建议在应用程序代码中选择一致的格式。

在 Azure 门户中将 Microsoft Graph API 范围添加到应用注册后,将以下应用设置配置添加到应用中的 wwwroot/appsettings.json 文件,其中包括具有 Microsoft Graph 版本和范围的 Graph 基 URL。 在下面的示例中,指定了 User.Read 范围以用于本文后面部分的示例。 范围不区分大小写。

"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}

在前面的示例中,{VERSION} 占位符是 Microsoft Graph API 的版本(例如:v1.0)。

下面是将 ME-ID 用作其 identity 提供者的应用的完整 wwwroot/appsettings.json 配置文件示例,其中为 Microsoft Graph 指定了读取用户数据(user.read 范围):

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}

在前面的示例中,{TENANT ID} 占位符是目录(租户)ID,{CLIENT ID} 占位符是应用程序(客户端)ID。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

将以下 GraphClientExtensions 类添加到独立应用。 向 AuthenticateRequestAsync 方法中 AccessTokenRequestOptionsScopes 属性提供范围。 IHttpProvider.OverallTimeout 从默认值 100 秒延长到 300 秒,使 HttpClient 有更多的时间接收来自 Microsoft Graph 的响应。

将以下 GraphClientExtensions 类添加到托管的 Blazor WebAssembly解决方案的独立应用或 Client 应用。 向 AuthenticateRequestAsync 方法中 AccessTokenRequestOptionsScopes 属性提供范围。 IHttpProvider.OverallTimeout 从默认值 100 秒延长到 300 秒,使 HttpClient 有更多的时间接收来自 Microsoft Graph 的响应。

如果没有获取访问令牌,以下代码不会为 Graph 请求设置持有者授权标头。

GraphClientExtensions.cs:

using System.Net.Http.Headers;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Authentication.WebAssembly.Msal.Models;
using Microsoft.Graph;

namespace BlazorSample;

internal static class GraphClientExtensions
{
    public static IServiceCollection AddGraphClient(
        this IServiceCollection services, string? baseUrl, List<string>? scopes)
    {
        if (string.IsNullOrEmpty(baseUrl) || scopes?.Count == 0)
        {
            return services;
        }

        services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
            options =>
            {
                scopes?.ForEach((scope) =>
                {
                    options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
                });
            });

        services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();

        services.AddScoped<IHttpProvider, HttpClientHttpProvider>(sp =>
            new HttpClientHttpProvider(new HttpClient()));

        services.AddScoped(sp =>
        {
            return new GraphServiceClient(
                baseUrl,
                sp.GetRequiredService<IAuthenticationProvider>(),
                sp.GetRequiredService<IHttpProvider>());
        });

        return services;
    }

    private class GraphAuthenticationProvider(IAccessTokenProvider tokenProvider, 
        IConfiguration config) : IAuthenticationProvider
    {
        private readonly IConfiguration config = config;

        public IAccessTokenProvider TokenProvider { get; } = tokenProvider;

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            var result = await TokenProvider.RequestAccessToken(
                new AccessTokenRequestOptions()
                { 
                    Scopes = config.GetSection("MicrosoftGraph:Scopes").Get<string[]>()
                });

            if (result.TryGetToken(out var token))
            {
                request.Headers.Authorization ??= new AuthenticationHeaderValue(
                    "Bearer", token.Value);
            }
        }
    }

    private class HttpClientHttpProvider(HttpClient client) : IHttpProvider
    {
        private readonly HttpClient client = client;

        public ISerializer Serializer { get; } = new Serializer();

        public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
        {
            return client.SendAsync(request);
        }

        public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            HttpCompletionOption completionOption,
            CancellationToken cancellationToken)
        {
            return client.SendAsync(request, completionOption, cancellationToken);
        }

        public void Dispose()
        {
        }
    }
}

重要

有关上述代码为何使用 DefaultAccessTokenScopes 而不是使用 AdditionalScopesToConsent 添加范围的说明,请参阅 DefaultAccessTokenScopesAdditionalScopesToConsent 部分。

Program 文件中,使用 AddGraphClient 扩展方法添加 Graph 客户端服务和配置:

var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

使用 Graph SDK 从组件调用 Graph API

以下 UserData 组件使用注入的 GraphServiceClient 来获取用户的 ME-ID 个人资料数据并显示其手机号码。 对于你在 ME-ID 中创建的任何测试用户,请确保在 Azure 门户中为用户的 ME-ID 个人资料提供一个手机号码。

UserData.razor:

@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Graph
@attribute [Authorize]
@inject GraphServiceClient Client

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(user?.MobilePhone))
{
    <p>Mobile Phone: @user.MobilePhone</p>
}

@code {
    private Microsoft.Graph.User? user;

    protected override async Task OnInitializedAsync()
    {
        var request = Client.Me.Request();
        user = await request.GetAsync();
    }
}

NavMenu 组件(Layout/NavMenu.razor)中添加指向组件页面的链接:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>

提示

要将用户添加到应用,请参阅“使用或不使用应用角色将用户分配到应用注册”部分。

在本地使用 Graph SDK 进行测试时,建议为每个测试使用一个新的专用/隐身浏览器会话,防止 Cookie 干扰测试的情况继续存在。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

使用 Graph SDK 自定义用户声明

在下面的示例中,应用基于用户 ME-ID 个人资料数据为用户创建手机号码和办公地点声明。 应用必须在 ME-ID 中配置User.Read Graph API 范围。 此方案的任何测试用户都必须在其 ME-ID 个人资料中包含手机号码和办公地点,可以通过 Azure 门户添加。

在以下自定义用户帐户工厂中:

  • 为了方便起见,我们包含了一个 ILogger (logger) ,便于你要在 CreateUserAsync 方法中记录信息或错误时使用。
  • 如果引发了 AccessTokenNotAvailableException,则会将用户重定向到 identity 提供程序以登录到其帐户。 如果请求访问令牌失败,可以采取其他不同的操作。 例如,应用可以记录 AccessTokenNotAvailableException 并创建支持票证以进一步调查。
  • 框架的 RemoteUserAccount 表示用户的帐户。 如果应用需要扩展 RemoteUserAccount 的自定义用户帐户类,请将下面代码中你的自定义用户帐户类替换为 RemoteUserAccount

CustomAccountFactory.cs:

using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
using Microsoft.Graph;

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor, 
        IServiceProvider serviceProvider, ILogger<CustomAccountFactory> logger)
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IServiceProvider serviceProvider = serviceProvider;

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity is not null && 
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null)
            {
                try
                {
                    var client = ActivatorUtilities
                        .CreateInstance<GraphServiceClient>(serviceProvider);
                    var request = client.Me.Request();
                    var user = await request.GetAsync();

                    if (user is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            user.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            user.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}

将 MSAL 身份验证配置为使用自定义用户帐户工厂。

确认 Program 文件使用 Microsoft.AspNetCore.Components.WebAssembly.Authentication 命名空间:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

本部分中的示例基于通过 wwwroot/appsettings.json 文件中的 MicrosoftGraph 节从应用配置中读取具有版本和范围的基 URL 的方法。 按照本文前面的指南,Program 文件中应已存在以下行:

var baseUrl = string.Join("/",
    builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
        "https://graph.microsoft.com",
    builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
        "v1.0");
var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
    .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddGraphClient(baseUrl, scopes);

Program 文件中,找到对 AddMsalAuthentication 扩展方法的调用。 将代码更新为以下内容,其中包括对 AddAccountClaimsPrincipalFactory 的调用,该调用使用 CustomAccountFactory 添加帐户声明主体工厂。

如果应用使用扩展 RemoteUserAccount 的自定义用户帐户类,请使用以下代码将自定义用户帐户类替换为 RemoteUserAccount

builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount,
        CustomAccountFactory>();

在用户使用 ME-ID 进行身份验证后,你可以使用以下UserClaims组件来研究用户的声明:

UserClaims.razor:

@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}
else
{
    <p>No claims found.</p>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}

NavMenu 组件(Layout/NavMenu.razor)中添加指向组件页面的链接:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>

在本地使用 Graph SDK 进行测试时,建议为每个测试使用一个新的专用/隐身浏览器会话,防止 Cookie 干扰测试的情况继续存在。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

以下示例使用已命名的 HttpClient 进行图形 API 调用,获取用户的手机号码来处理呼叫或自定义用户的声明,以包括手机号码声明和办公地点声明。

这些示例要求独立 Blazor WebAssembly 应用具有 Microsoft.Extensions.Http 的包引用。

示例需要独立 Blazor WebAssembly 应用或托管的 Blazor WebAssembly 解决方案 Client 应用中 Microsoft.Extensions.Http 的包引用。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

在 Azure 门户中,授予对应用应能够代表用户访问的 Microsoft Graph 数据的委托访问权限(范围)†。 对于本文中的示例,应用注册应包括读取用户数据的委托权限(API 权限中的 Microsoft.Graph>User.Read 范围,类型:委派)。 通过 User.Read 范围,用户能够登录到应用,并且应用可以读取已登录用户的个人资料和公司信息。 有关详细信息,请参阅 Microsoft identity 平台中的权限和同意概述以及 Microsoft Graph 权限概述

†权限和范围是同一个意思,在安全文档和 Azure 门户中可以互换使用。 除非文本引用 Azure 门户,否则本文在引用 Graph 权限时使用作用域/作用域

范围不区分大小写,因此 User.Readuser.read 相同。 请随意使用任一格式,但建议在应用程序代码中选择一致的格式。

在 Azure 门户中将 Microsoft Graph API 范围添加到应用注册后,将以下应用设置配置添加到应用中的 wwwroot/appsettings.json 文件,其中包括具有 Microsoft Graph 版本和范围的 Graph 基 URL。 在下面的示例中,指定了 User.Read 范围以用于本文后面部分的示例。 范围不区分大小写。

"MicrosoftGraph": {
  "BaseUrl": "https://graph.microsoft.com",
  "Version": "{VERSION}",
  "Scopes": [
    "user.read"
  ]
}

在前面的示例中,{VERSION} 占位符是 Microsoft Graph API 的版本(例如:v1.0)。

下面是将 ME-ID 用作其 identity 提供者的应用的完整 wwwroot/appsettings.json 配置文件示例,其中为 Microsoft Graph 指定了读取用户数据(user.read 范围):

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/{TENANT ID}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": true
  },
  "MicrosoftGraph": {
    "BaseUrl": "https://graph.microsoft.com",
    "Version": "v1.0",
    "Scopes": [
      "user.read"
    ]
  }
}

在前面的示例中,{TENANT ID} 占位符是目录(租户)ID,{CLIENT ID} 占位符是应用程序(客户端)ID。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

Program 文件中创建以下 GraphAuthorizationMessageHandler 类和项目配置,以便使用 Graph API。 基 URL 和范围通过配置提供给处理程序。

GraphAuthorizationMessageHandler.cs:

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

namespace BlazorSample;

public class GraphAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public GraphAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigation, IConfiguration config)
        : base(provider, navigation)
    {
        ConfigureHandler(
            authorizedUrls: [ 
                string.Join("/",
                    config.GetSection("MicrosoftGraph")["BaseUrl"] ??
                        "https://graph.microsoft.com",
                    config.GetSection("MicrosoftGraph")["Version"] ??
                        "v1.0")
            ],
            scopes: config.GetSection("MicrosoftGraph:Scopes")
                        .Get<List<string>>() ?? [ "user.read" ]);
    }
}

需要授权 URL 后面的斜杠 (/)。 上述代码根据应用设置配置生成以下授权 URL,如果缺少应用设置配置,则默认为以下授权 URL:https://graph.microsoft.com/v1.0/

Program 文件中,为 Graph API 配置命名 HttpClient

builder.Services.AddTransient<GraphAuthorizationMessageHandler>();

builder.Services.AddHttpClient("GraphAPI",
        client => client.BaseAddress = new Uri(
            string.Join("/",
                builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"] ??
                    "https://graph.microsoft.com",
                builder.Configuration.GetSection("MicrosoftGraph")["Version"] ??
                    "v1.0",
                string.Empty)))
    .AddHttpMessageHandler<GraphAuthorizationMessageHandler>();

在前面的示例中,GraphAuthorizationMessageHandler DelegatingHandler 被注册为 AddHttpMessageHandler 的临时服务。 建议对 IHttpClientFactory 进行临时注册,用于管理其自己的 DI 范围。 有关详细信息,请参阅以下资源:

需要基址后面的斜杠 (/)。 在上述代码中,string.Join 的第三个参数为 string.Empty,以确保后面的斜杠存在:https://graph.microsoft.com/v1.0/

使用已命名的 HttpClient 从组件调用图形 API

UserInfo.cs 类指定所需的用户个人资料属性(通过 JsonPropertyNameAttribute 特性以及 ME-ID 使用的 JSON 名称来实现)。 以下示例为用户的手机号码和办公地点设置属性。

UserInfo.cs:

using System.Text.Json.Serialization;

namespace BlazorSample;

public class UserInfo
{
    [JsonPropertyName("mobilePhone")]
    public string? MobilePhone { get; set; }

    [JsonPropertyName("officeLocation")]
    public string? OfficeLocation { get; set; }
}

在以下 UserData 组件中,HttpClient 是为图形 API 创建的,用于发出对用户个人资料数据的请求。 me 资源 (me) 将添加到图形 API 请求的具有版本的基 URL 中。 Graph 返回的 JSON 数据将反序列化为 UserInfo 类属性。 以下示例获取手机号码。 如果你希望获取 (userInfo.OfficeLocation),可以添加类似的代码以包括用户的 ME-ID 个人资料办公地点。 如果访问令牌请求失败,则会重定向用户以登录到应用以获取新的访问令牌。

UserData.razor:

@page "/user-data"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@attribute [Authorize]
@inject IConfiguration Config
@inject IHttpClientFactory ClientFactory

<PageTitle>User Data</PageTitle>

<h1>Microsoft Graph User Data</h1>

@if (!string.IsNullOrEmpty(userInfo?.MobilePhone))
{
    <p>Mobile Phone: @userInfo.MobilePhone</p>
}

@code {
    private UserInfo? userInfo;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("GraphAPI");

            userInfo = await client.GetFromJsonAsync<UserInfo>("me");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

NavMenu 组件(Layout/NavMenu.razor)中添加指向组件页面的链接:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-data">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Data
    </NavLink>
</div>

提示

要将用户添加到应用,请参阅“使用或不使用应用角色将用户分配到应用注册”部分。

以下序列描述了图形 API 范围的新用户流:

  1. 新用户首次登录到应用。
  2. 用户同意在 Azure 同意 UI 中使用应用。
  3. 用户首次访问请求图形 API 数据的组件页。
  4. 系统将用户重定向到 Azure 同意 UI,以同意图形 API 范围。
  5. 返回图形 API 用户数据。

如果想要在初始登录时进行范围预配(同意图形 API 范围),请提供 MSAL 身份验证的范围作为 Program 文件中的默认访问令牌范围:

+ var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
+     .Get<List<string>>() ?? [ "user.read" ];

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);

+   foreach (var scope in scopes)
+   {
+       options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
+   }
});

重要

有关上述代码为何使用 DefaultAccessTokenScopes 而不是使用 AdditionalScopesToConsent 添加范围的说明,请参阅 DefaultAccessTokenScopesAdditionalScopesToConsent 部分。

对应用进行上述更改时,用户流采用以下顺序:

  1. 新用户首次登录到应用。
  2. 用户同意在 Azure 同意 UI 中使用应用和图形 API 范围。
  3. 用户首次访问请求图形 API 数据的组件页。
  4. 返回图形 API 用户数据。

在本地使用 Graph API 进行测试时,建议为每个测试使用一个新的专用/隐身浏览器会话,防止 Cookie 干扰测试的情况继续存在。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

使用已命名的 HttpClient 自定义用户声明

在下面的示例中,应用基于用户 ME-ID 个人资料数据为用户创建手机号码和办公地点声明。 应用必须在 ME-ID 中配置User.Read Graph API 范围。 ME-ID 中的测试用户帐户需要手机号码和办公地点条目,此条目可以通过 Azure 门户添加到其用户个人资料中。

如果你尚未按照本文前面的指南将 UserInfo 类添加到应用,请添加以下类,并使用 JsonPropertyNameAttribute 属性和 ME-ID 使用的 JSON 名称指定所需的用户个人资料属性。 以下示例为用户的手机号码和办公地点设置属性。

UserInfo.cs:

using System.Text.Json.Serialization;

namespace BlazorSample;

public class UserInfo
{
    [JsonPropertyName("mobilePhone")]
    public string? MobilePhone { get; set; }

    [JsonPropertyName("officeLocation")]
    public string? OfficeLocation { get; set; }
}

在以下自定义用户帐户工厂中:

  • 为了方便起见,我们包含了一个 ILogger (logger) ,便于你要在 CreateUserAsync 方法中记录信息或错误时使用。
  • 如果引发了 AccessTokenNotAvailableException,则会将用户重定向到 identity 提供程序以登录到其帐户。 如果请求访问令牌失败,可以采取其他不同的操作。 例如,应用可以记录 AccessTokenNotAvailableException 并创建支持票证以进一步调查。
  • 框架的 RemoteUserAccount 表示用户的帐户。 如果应用需要扩展 RemoteUserAccount 的自定义用户帐户类,请将下面代码中的自定义用户帐户类替换为 RemoteUserAccount

CustomAccountFactory.cs:

using System.Net.Http.Json;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

public class CustomAccountFactory(IAccessTokenProviderAccessor accessor,
        IHttpClientFactory clientFactory,
        ILogger<CustomAccountFactory> logger)
    : AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
{
    private readonly ILogger<CustomAccountFactory> logger = logger;
    private readonly IHttpClientFactory clientFactory = clientFactory;

    public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
        RemoteUserAccount account,
        RemoteAuthenticationUserOptions options)
    {
        var initialUser = await base.CreateUserAsync(account, options);

        if (initialUser.Identity is not null && 
            initialUser.Identity.IsAuthenticated)
        {
            var userIdentity = initialUser.Identity as ClaimsIdentity;

            if (userIdentity is not null)
            {
                try
                {
                    var client = clientFactory.CreateClient("GraphAPI");

                    var userInfo = await client.GetFromJsonAsync<UserInfo>("me");

                    if (userInfo is not null)
                    {
                        userIdentity.AddClaim(new Claim("mobilephone",
                            userInfo.MobilePhone ?? "(000) 000-0000"));
                        userIdentity.AddClaim(new Claim("officelocation",
                            userInfo.OfficeLocation ?? "Not set"));
                    }
                }
                catch (AccessTokenNotAvailableException exception)
                {
                    exception.Redirect();
                }
            }
        }

        return initialUser;
    }
}

MSAL 身份验证配置为使用自定义用户帐户工厂。 首先确认 Program 文件使用 Microsoft.AspNetCore.Components.WebAssembly.Authentication 命名空间:

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

Program 文件中,找到对 AddMsalAuthentication 扩展方法的调用。 将代码更新为以下内容,其中包括对 AddAccountClaimsPrincipalFactory 的调用,该调用使用 CustomAccountFactory 添加帐户声明主体工厂。

如果应用使用扩展 RemoteUserAccount 的自定义用户帐户类,请使用以下代码将你的应用的自定义用户帐户类替换为 RemoteUserAccount

builder.Services.AddMsalAuthentication<RemoteAuthenticationState, 
    RemoteUserAccount>(options =>
    {
        builder.Configuration.Bind("AzureAd", 
            options.ProviderOptions.Authentication);
    })
    .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount, 
        CustomAccountFactory>();

前面的示例适用于使用 ME-ID 身份验证和 MSAL 的应用。 对于 OIDC 和 API 身份验证,也存在类似的模式。 有关详细信息,请参阅 ASP.NET Core Blazor WebAssembly 其他安全方案一文中的使用有效负载声明自定义用户部分。

在用户使用 ME-ID 进行身份验证后,你可以使用以下UserClaims组件来研究用户的声明:

UserClaims.razor:

@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>User Claims</h1>

@if (claims.Any())
{
    <ul>
        @foreach (var claim in claims)
        {
            <li>@claim.Type: @claim.Value</li>
        }
    </ul>
}
else
{
    <p>No claims found.</p>
}

@code {
    private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

    protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider
            .GetAuthenticationStateAsync();
        var user = authState.User;

        claims = user.Claims;
    }
}

NavMenu 组件(Layout/NavMenu.razor)中添加指向组件页面的链接:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="user-claims">
        <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> User Claims
    </NavLink>
</div>

在本地使用 Graph API 进行测试时,建议为每个测试使用一个新的专用/隐身浏览器会话,防止 Cookie 干扰测试的情况继续存在。 有关详细信息,请参阅使用 Microsoft Entra ID 保护 ASP.NET Core Blazor WebAssembly 独立应用

使用或不使用应用角色将用户分配到应用注册

可以在 Azure 门户中通过以下步骤将用户添加到应用注册并向用户分配角色。

要添加用户,请从 Azure 门户的 ME-ID 区域选择“用户”

  1. 选择“新建用户”>“创建新用户”。
  2. 使用“创建用户”模板
  3. Identity 区域中提供用户的信息。
  4. 可以生成初始密码,或分配用户首次登录时更改的初始密码。 如果使用门户生成的密码,请立即记下密码。
  5. 选择“创建”以创建用户。 “创建新用户”界面关闭时,选择“刷新”以更新用户列表并显示新用户。
  6. 对于本文中的示例,请通过从用户列表中选择其姓名、选择“属性”和编辑联系人信息提供移动电话号码来为新用户分配移动电话号码。

要将用户分配到没有应用角色的应用,请:

  1. 在 Azure 门户的 ME-ID 区域中,打开“企业应用程序”。
  2. 从列表中选择应用。
  3. 选择“用户和组”。
  4. 选择“添加用户/组”。
  5. 选择用户。
  6. 选择“分配”按钮。

要将用户分配到具有应用角色的应用,请执行以下操作:

  1. 按照使用 Microsoft Entra ID 组和角色的 ASP.NET Core Blazor WebAssembly 中的指导,在 Azure 门户中将角色添加到应用的注册。
  2. 在 Azure 门户的 ME-ID 区域中,打开“企业应用程序”。
  3. 从列表中选择应用。
  4. 选择“用户和组”。
  5. 选择“添加用户/组”。
  6. 选择用户并选择其用于访问应用的角色。 通过重复将用户添加到应用的过程,将多个角色分配给用户,直到分配了用户的所有角色。 在应用用户的“用户和组”列表中,具有多个角色的用户会针对每个分配的角色列出一次。
  7. 选择“分配”按钮。

DefaultAccessTokenScopesAdditionalScopesToConsent

本文中的示例使用了 DefaultAccessTokenScopes 而不是 AdditionalScopesToConsent 来预配图形 API 范围。

不使用 AdditionalScopesToConsent 是因为当用户首次通过 Azure 同意 UI 使用 MSAL 登录到应用时,它无法为用户预配图形 API 范围。 当用户首次尝试使用 Graph SDK 访问图形 API 时,他们将遇到异常:

Microsoft.Graph.Models.ODataErrors.ODataError: Access token is empty.

在用户预配通过 DefaultAccessTokenScopes 提供的图形 API 范围后,应用可将 AdditionalScopesToConsent 用于后续用户登录。 但更改应用代码对于需要定期添加具有委托图形范围的新用户或向应用添加新委托图形 API 范围的生产应用毫无意义。

上文对在用户首次登录应用时如何预配图形 API 访问范围的讨论仅适用于:

  • 采用 Graph SDK 的应用。
  • 使用要求用户在首次登录应用时同意 Graph 范围的已命名 HttpClient 进行图形 API 访问的用户。

如果使用的已命名 HttpClient 不要求用户在首次登录时同意 Graph 范围,则当用户首次通过预配置的 DelegatingHandler 请求访问图形 API 时,系统会将用户重定向到图形 API 范围同意的 Azure 同意 UI,也就是 HttpClient。 当 Graph 范围最初未通过已命名 HttpClient 方法获得同意时,应用不会调用 DefaultAccessTokenScopes,也不会调用 AdditionalScopesToConsent。 有关详细信息,请参阅本文中命名 HttpClient 覆盖范围

托管的 Blazor WebAssembly 解决方案

本文中的示例涉及直接从独立 Blazor WebAssembly 应用或直接从托管的 Blazor WebAssembly解决方案Client 应用使用 Graph SDK 或使用图形 API 的已命名 HttpClient。 本文没有介绍的另一个场景是,托管解决方案的 Client 应用通过 Web API 调用解决方案的 Server 应用,然后 Server 应用使用 Graph SDK/API 调用 Microsoft Graph 并将数据返回到 Client 应用。 虽然这种方法受支持,但本文未对此进行介绍。 如果你希望采用此方法,请执行以下操作:

  • 按照从 ASP.NET Core Blazor 应用调用 Web API 中的指南操作,使用 Web API 从 Client 应用向 Server 应用发出请求,并将数据返回到 Client 应用。
  • 按照主要的 Microsoft Graph 文档中的指导,将 Graph SDK 与典型的 ASP.NET Core 应用结合使用,在此方案中,该应用是解决方案的 Server 应用。 如果使用 Blazor WebAssembly 项目模板创建具有组织授权(单个组织/SingleOrg 或多个组织/MultiOrg)和 Microsoft Graph 选项(Visual Studio 中的“Microsoft identity 平台”>“连接的服务”>“添加 Microsoft Graph 权限”或使用 .NET CLI dotnet new 命令的 --calls-graph 选项)的托管的 Blazor WebAssembly 解决方案(ASP.NET Core 托管的 /-h|--hosted),则解决方案的 Server 应用配置为在从项目模板创建解决方案时使用 Graph SDK。

其他资源

一般指南

安全指南