将Microsoft Entra ID身份验证添加到.NET Aspire应用

本指南演示如何使用 Microsoft Entra ID 身份验证和授权,保护 .NET Aspire 分布式应用程序。 它涵盖:

  1. Blazor 服务器前端MyService.Web):使用 OpenID Connect 和令牌获取用户登录
  2. 受保护的 API 后端MyService.ApiService):使用 Microsoft.Identity.Web 进行 JWT 验证
  3. 端到端流程:Blazor 获取访问令牌,然后通过 Aspire 服务发现来调用受保护的 API

本指南假定你已使用以下命令创建 Aspire 项目:

aspire new aspire-starter --name MyService

先决条件

小窍门

您是 Aspire 的新用户吗? 请参阅 .NET Aspire 概述

了解两阶段工作流

本指南遵循两个阶段的方法:

阶段 发生的情况 结果
阶段 1 添加含占位符值的认证代码 应用程序能够构建,但无法运行
阶段 2 配置 Microsoft Entra 应用注册 应用使用实际身份验证运行

在 Microsoft Entra ID 中注册应用

在应用对用户进行身份验证之前,需要在Microsoft Entra中注册两个应用:

应用注册 Purpose 键配置
APIMyService.ApiService 验证传入令牌 应用 ID URI、 access_as_user 范围
Web 应用MyService.Web 让用户登录,获取令牌 重定向 URI、客户端密码、API 权限

如果已配置应用注册,则需要以下值:appsettings.json

  • TenantId — Microsoft Entra租户 ID
  • API ClientId — API 应用注册的应用程序(客户端)ID
  • API 应用 ID URI - 通常 api://<api-client-id> (用于 AudiencesScopes
  • Web 应用客户端 ID — Web 应用注册的应用程序(客户端)ID
  • 客户端密码 (或证书) - Web 应用的凭据(存储在用户机密中,而不是 appsettings.json)
  • 范围 — 您的 Web 应用程序请求的范围,例如api://<api-client-id>/.defaultapi://<api-client-id>/access_as_user

步骤 1:注册 API

  1. 转到 Microsoft Entra 管理中心>身份>应用程序>应用注册
  2. 选择新注册
    • 名称:MyService.ApiService
    • 支持的帐户类型: 仅限此组织目录中的帐户(单租户)
    • 选择注册
  3. 转到公开 API>在应用程序 ID URI 旁边添加
    • 接受默认值(api://<client-id>)或对其进行自定义。
    • 选择 “添加范围
      • 范围名称:access_as_user
      • 谁可以同意: 管理员和用户
      • 管理员同意显示名称: 访问 MyService API
      • 管理员同意说明: 允许应用代表已登录用户访问 MyService API。
      • 选择添加作用域
  4. 复制 应用程序(客户端)ID,因为你需要在两个 appsettings.json 文件中使用它。

有关详细信息,请参阅 快速入门:配置应用以公开 Web API

步骤 2:注册 Web 应用

  1. 转到 应用注册>新注册
    • 名称:MyService.Web
    • 支持的帐户类型: 仅限此组织目录中的帐户
    • 重定向 URI: 选择 Web 并输入应用的 URL + /signin-oidc
      • 本地开发时:https://localhost:7001/signin-oidc (请检查 launchSettings.json 的实际端口)
    • 选择注册
  2. 转到 “身份验证”>添加 URI 以添加您所有的开发 URI(launchSettings.json)。
  3. 转到 证书和机密>客户端机密>“新建客户端机密”。
    • 添加说明和过期时间。
    • 立即复制机密值 - 它不会再次显示。
  4. 转到 API 权限>,添加权限>,然后选择我的 API
    • 选择 MyService.ApiService
    • 选择“ access_as_user>添加权限”。
    • [租户] 选择“授予管理员同意”选项(或者在首次使用时将提示用户)。
  5. 复制 Web 应用appsettings.json

注释

某些组织不允许客户端机密。 有关替代方法,请参阅 证书凭据无证书身份验证

有关详细信息,请参阅快速入门:注册应用程序

步骤 3:更新配置

创建应用注册后,更新 appsettings.json 文件:

API(MyService.ApiService/appsettings.json):

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "YOUR_TENANT_ID",
    "ClientId": "YOUR_API_CLIENT_ID",
    "Audiences": ["api://YOUR_API_CLIENT_ID"]
  }
}

Web 应用(MyService.Web/appsettings.json):

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "YOUR_TENANT_ID",
    "ClientId": "YOUR_WEB_CLIENT_ID",
    "CallbackPath": "/signin-oidc",
    "ClientCredentials": [
      { "SourceType": "ClientSecret" }
    ]
  },
  "WeatherApi": {
    "Scopes": ["api://YOUR_API_CLIENT_ID/.default"]
  }
}

安全地存储机密:

cd MyService.Web
dotnet user-secrets set "AzureAd:ClientCredentials:0:ClientSecret" "YOUR_SECRET_VALUE"
价值 在何处查找
TenantId Microsoft Entra 管理中心 > 概述 > 租户 ID
API ClientId 应用注册 > MyService.ApiService > 应用程序(客户端)ID
Web ClientId 应用程序注册 > MyService.Web > 应用程序(客户端)ID
Client Secret 在步骤 2 中创建(创建时立即复制)

注释

Aspire 入门模板会在WeatherApiClient项目中自动创建MyService.Web类。 本指南中使用此类型化的 HttpClient 来演示如何调用受保护的 API。 无需自行创建此类,它已经是模板的一部分。


快速入门

本部分提供用于添加身份验证的精简参考。 有关详细演练,请参阅 第 1 部分 和第 2 部分

API (MyService.ApiService

安装Microsoft。Identity.Web NuGet 包:

dotnet add package Microsoft.Identity.Web

将 Microsoft Entra 配置添加到 appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-id>",
    "ClientId": "<api-client-id>",
    "Audiences": ["api://<api-client-id>"]
  }
}

Program.cs中注册身份验证和授权:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization();
// ...
app.UseAuthentication();
app.UseAuthorization();
// ...
app.MapGet("/weatherforecast", () => { /* ... */ }).RequireAuthorization();

Web 应用 (MyService.Web

安装Microsoft。Identity.Web NuGet 包:

dotnet add package Microsoft.Identity.Web

将 Microsoft Entra 配置添加到 appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-id>",
    "ClientId": "<web-client-id>",
    "CallbackPath": "/signin-oidc",
    "ClientCredentials": [{ "SourceType": "ClientSecret" }]
  },
  "WeatherApi": { "Scopes": ["api://<api-client-id>/.default"] }
}

Program.cs中配置身份验证、获取令牌和下游 API 客户端:

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<BlazorAuthenticationChallengeHandler>();

builder.Services.AddHttpClient<WeatherApiClient>(client =>
    client.BaseAddress = new("https+http://apiservice"))
    .AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi"));
// ...
app.UseAuthentication();
app.UseAuthorization();
app.MapGroup("/authentication").MapLoginAndLogout();

自动MicrosoftIdentityMessageHandler负责获取和附加令牌,BlazorAuthenticationChallengeHandler负责处理同意和条件访问挑战。

重要

不要忘记为登录按钮创建 UserInfo.razor 。 有关详细信息,请参阅 “添加 Blazor UI 组件 ”。

注释

BlazorAuthenticationChallengeHandlerLoginLogoutEndpointRouteBuilderExtensions 在 Microsoft.Identity.Web(v3.3.0+)中推出。 无需复制文件。


标识要修改的文件

下表列出了在每个项目中更改的文件:

项目 File Changes
ApiService Program.cs JWT 持有者身份验证、授权中间件
appsettings.json Microsoft Entra配置
.csproj 添加 Microsoft.Identity.Web
网络 Program.cs OIDC 身份验证,令牌获取,BlazorAuthenticationChallengeHandler
appsettings.json Microsoft Entra 配置、下游 API 权限范围
.csproj 添加 Microsoft.Identity.Web (v3.3.0+)
Components/UserInfo.razor 登录按钮 UI (新文件)
Components/Layout/MainLayout.razor 包括 UserInfo 组件
Components/Routes.razor 为受保护的页面授权RouteView
调用 API 的页面 使用 ChallengeHandler 试用/捕获

了解身份验证流

下图显示了 Blazor 前端、Microsoft Entra和受保护 API 的交互方式:

flowchart LR
  A[User Browser] -->|1 Login OIDC| B[Blazor Server<br/>MyService.Web]
  B -->|2 Redirect| C[Microsoft Entra ID]
  C -->|3 auth code| B
  B -->|4 exchange auth code| C
  C -->|5 tokens| B
  B -->|6 cookie + session| A
  B -->|7 HTTP + Bearer token| D[ASP.NET API<br/>MyService.ApiService<br/>Microsoft.Identity.Web]
  D -->|8 Validate JWT| C
  D -->|9 Weather data| B
  1. 用户访问 Blazor 应用 →未通过身份验证→看到“登录”按钮。
  2. 用户选择登录 → 重定向到 /authentication/login → OIDC 验证挑战 → Microsoft Entra。
  3. 用户登录 → Microsoft Entra 重定向 /signin-oidc → cookie 已建立。
  4. 用户导航到天气页面 → Blazor 调用WeatherApiClient.GetAsync()
  5. MicrosoftIdentityMessageHandler 截获请求,从缓存获取令牌(或以无提示方式刷新),并附加 Authorization: Bearer <token> 标头。
  6. API 接收请求 → Microsoft。Identity.Web 验证 JWT →返回数据。
  7. Blazor 呈现天气数据

查看解决方案结构

Aspire 初学者模板创建以下项目布局:

MyService/
├── MyService.AppHost/           # Aspire orchestration
├── MyService.ApiService/        # Protected API (Microsoft.Identity.Web)
├── MyService.Web/               # Blazor Server (Microsoft.Identity.Web)
├── MyService.ServiceDefaults/   # Shared defaults
└── MyService.Tests/             # Tests

将 Microsoft.Identity.Web 应用于 API 后端以确保安全。

本部分将 API 项目配置为验证由Microsoft Entra颁发的 JWT 持有者令牌。

添加Microsoft。Identity.Web 包

运行以下命令以安装Microsoft。Identity.Web NuGet 包:

cd MyService.ApiService
dotnet add package Microsoft.Identity.Web

配置 Microsoft Entra 设置

将 Microsoft Entra 配置添加到 MyService.ApiService/appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<your-tenant-id>",
    "ClientId": "<your-api-client-id>",
    "Audiences": [
      "api://<your-api-client-id>"
    ]
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

关键属性:

  • ClientId:Microsoft Entra API 应用注册 ID
  • TenantId:您的 Microsoft Entra 租户 ID,或 "organizations" 用于多租户,或 "common" 用于任何 Microsoft 帐户
  • Audiences:有效的令牌受众(通常是应用 ID 的 URI)

更新 API Program.cs

MyService.ApiService/Program.cs 以下内容替换为以下代码,以添加 JWT 持有者身份验证和保护终结点:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Add Microsoft.Identity.Web JWT Bearer authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseExceptionHandler();
app.UseAuthentication();
app.UseAuthorization();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild",
    "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

app.MapGet("/", () =>
    "API service is running. Navigate to /weatherforecast to see sample data.");

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.RequireAuthorization();

app.MapDefaultEndpoints();
app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

关键更改:

  • 使用 AddMicrosoftIdentityWebApi 注册 JWT Bearer 身份验证
  • 添加 app.UseAuthentication()app.UseAuthorization() 中间件
  • .RequireAuthorization() 应用于受保护的终结点

测试受保护的 API

验证 API 是否拒绝未经身份验证的请求并接受有效的令牌。

发送没有令牌的请求:

curl https://localhost:<PORT>/weatherforecast
# Expected: 401 Unauthorized

使用有效令牌发送请求:

curl -H "Authorization: Bearer <TOKEN>" https://localhost:<PORT>/weatherforecast
# Expected: 200 OK with weather data

第 2 部分:配置 Blazor 前端以进行身份验证

Blazor Server 应用使用 Microsoft.Identity.Web

  • 通过 OIDC 验证用户身份
  • 获取访问令牌以调用 API
  • 将令牌添加到传出的 HTTP 请求

添加Microsoft。Identity.Web 包

运行以下命令以安装Microsoft。Identity.Web NuGet 包:

cd MyService.Web
dotnet add package Microsoft.Identity.Web

配置 Microsoft Entra 参数

将Microsoft Entra配置和下游 API 范围添加到 MyService.Web/appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<your-tenant>.onmicrosoft.com",
    "TenantId": "<tenant-guid>",
    "ClientId":  "<web-app-client-id>",
    "CallbackPath": "/signin-oidc",
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret",
        "ClientSecret": "<your-client-secret>"
      }
    ]
  },
  "WeatherApi": {
    "Scopes": [ "api://<api-client-id>/.default" ]
  },
  "Logging": {
    "LogLevel":  {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

配置详细信息:

  • ClientId:Web 应用注册 ID(而不是 API ID)
  • ClientCredentials:Web 应用程序用来获取令牌的凭据。 支持多种凭据类型。 有关生产就绪选项,请参阅 凭据概述
  • Scopes:必须使 API 的应用 ID URI 与 /.default 后缀匹配

警告

对于生产,请使用证书或托管标识,而不是客户端机密。 有关建议的方法,请参阅 无证书身份验证

更新 Web 应用Program.cs

MyService.Web/Program.cs 的内容替换为以下代码,以配置 OIDC 身份验证、令牌获取和下游 API 客户端:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Abstractions;
using Microsoft.Identity.Web;
using MyService.Web;
using MyService.Web.Components;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

// Authentication + Microsoft Identity Web
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

builder.Services.AddCascadingAuthenticationState();

// Blazor components
builder.Services.AddRazorComponents().AddInteractiveServerComponents();

// Blazor authentication challenge handler for incremental consent and Conditional Access
builder.Services.AddScoped<BlazorAuthenticationChallengeHandler>();

builder.Services.AddOutputCache();

// Downstream API client with MicrosoftIdentityMessageHandler
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
    // Aspire service discovery: resolves "apiservice" at runtime
    client.BaseAddress = new("https+http://apiservice");
})
.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi"));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.UseOutputCache();

app.MapStaticAssets();
app.MapRazorComponents<App>()
   .AddInteractiveServerRenderMode();

// Login/Logout endpoints with incremental consent support
app.MapGroup("/authentication").MapLoginAndLogout();

app.MapDefaultEndpoints();
app.Run();

要点:

  • AddMicrosoftIdentityWebApp:配置 OIDC 身份验证
  • EnableTokenAcquisitionToCallDownstreamApi:为下游的 API 获取令牌
  • AddScoped<BlazorAuthenticationChallengeHandler>:在 Blazor 服务器中处理增量许可和条件访问
  • AddMicrosoftIdentityMessageHandler:自动将持有者令牌附加到 HttpClient 请求
  • https+http://apiservice:Aspire 服务发现将此解析为实际的 API URL
  • 中间件顺序UseAuthentication()UseAuthorization() →终结点

AddMicrosoftIdentityMessageHandler 扩展支持多个配置模式:

选项 1:来自 appsettings.json 的配置(如前所示)

.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi"));

选项 2:使用操作委托进行内联配置

.AddMicrosoftIdentityMessageHandler(options =>
{
    options.Scopes.Add("api://<api-client-id>/.default");
});

选项 3:每个请求配置(无参数)

.AddMicrosoftIdentityMessageHandler();

// Then in your service, configure per-request:
var request = new HttpRequestMessage(HttpMethod.Get, "/weatherforecast")
    .WithAuthenticationOptions(options =>
    {
        options.Scopes.Add("api://<api-client-id>/.default");
    });
var response = await _httpClient.SendAsync(request);

添加 Blazor UI 组件

重要

此步骤经常被遗忘。 如果没有 UserInfo 组件,则用户无法登录。

Microsoft.Identity.Web v3.3.0+ 中发货。 一旦引用该包,它们便会自动可用,无需复制文件。

创建 MyService.Web/Components/UserInfo.razor

@using Microsoft.AspNetCore.Components.Authorization

<AuthorizeView>
    <Authorized>
        <span class="nav-item">Hello, @context.User.Identity?.Name</span>
        <form action="/authentication/logout" method="post" class="nav-item">
            <AntiforgeryToken />
            <input type="hidden" name="returnUrl" value="/" />
            <button type="submit" class="btn btn-link nav-link">Logout</button>
        </form>
    </Authorized>
    <NotAuthorized>
        <a href="/authentication/login?returnUrl=/" class="nav-link">Login</a>
    </NotAuthorized>
</AuthorizeView>

添加到布局:<UserInfo />中包含MainLayout.razor

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <UserInfo />
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

更新 AuthorizeRouteView 的 Route.razor

替换 RouteViewAuthorizeRouteViewComponents/Routes.razor:

@using Microsoft.AspNetCore.Components.Authorization

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
            <NotAuthorized>
                <p>You are not authorized to view this page.</p>
                <a href="/authentication/login">Login</a>
            </NotAuthorized>
        </AuthorizeRouteView>
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

处理调用 API 的页面上的异常

Blazor Server 需要对条件访问和许可进行显式的异常处理。 必须在每个调用下游 API 的页面上处理 MicrosoftIdentityWebChallengeUserException ,除非您的应用经过预授权,并且提前在 Program.cs 请求所有访问权限范围。

以下示例 Weather.razor 演示了正确的异常处理:

@page "/weather"
@attribute [Authorize]

@using Microsoft.AspNetCore.Authorization
@using Microsoft.Identity.Web

@inject WeatherApiClient WeatherApi
@inject BlazorAuthenticationChallengeHandler ChallengeHandler

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

@if (!string.IsNullOrEmpty(errorMessage))
{
    <div class="alert alert-warning">@errorMessage</div>
}
else if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;
    private string? errorMessage;

    protected override async Task OnInitializedAsync()
    {
        if (!await ChallengeHandler.IsAuthenticatedAsync())
        {
            await ChallengeHandler.ChallengeUserWithConfiguredScopesAsync("WeatherApi:Scopes");
            return;
        }

        try
        {
            forecasts = await WeatherApi.GetWeatherAsync();
        }
        catch (Exception ex)
        {
            // Handle incremental consent / Conditional Access
            if (!await ChallengeHandler.HandleExceptionAsync(ex))
            {
                errorMessage = $"Error loading weather data: {ex.Message}";
            }
        }
    }
}

该模式的工作原理如下:

  1. IsAuthenticatedAsync() 在进行 API 调用之前,检查用户是否已登录。
  2. HandleExceptionAsync() catches MicrosoftIdentityWebChallengeUserException (或作为 InnerException)。
  3. 如果是身份验证挑战异常,则用户将被重定向以重新使用所需的范围或声明进行身份验证。
  4. 如果这不是质询异常,则HandleExceptionAsync返回false,以便进行错误处理。

将客户端密码存储在用户机密中

使用 .NET 机密管理器在开发期间安全地存储客户端机密。

注意

切勿将敏感信息提交到版本控制系统。

初始化用户机密并存储客户端密码:

cd MyService.Web
dotnet user-secrets init
dotnet user-secrets set "AzureAd:ClientCredentials:0:ClientSecret" "<your-client-secret>"

然后更新 appsettings.json 以删除硬编码的机密:

{
  "AzureAd": {
    "ClientCredentials": [
      {
        "SourceType": "ClientSecret"
      }
    ]
  }
}

Microsoft。Identity.Web 支持多种凭据类型。 有关生产,请参阅 凭据概述


验证实现

使用此清单确认已完成所有必需的步骤。

API 项目

  • [ ] 添加了 Microsoft.Identity.Web
  • [ ] 更新appsettings.json以包含AzureAd
  • [ ] 使用 Program.cs 更新了 AddMicrosoftIdentityWebApi
  • [ ] 将 .RequireAuthorization() 添加到受保护的终结点

Web/Blazor 项目

  • [ ] 添加了 Microsoft.Identity.Web 包 (v3.3.0+)
  • [ ] 用appsettings.jsonAzureAd节更新了WeatherApi
  • [ ] 使用 OIDC 更新Program.cs,并获取令牌
  • [ ] 已添加 AddScoped<BlazorAuthenticationChallengeHandler>()
  • [ ] 已创建 Components/UserInfo.razor (登录按钮)
  • [ ] 已更新 MainLayout.razor 为包含 <UserInfo />
  • [ ] 使用 Routes.razor 更新了 AuthorizeRouteView
  • [ ] 在每个调用 API 的页面上添加了带有 ChallengeHandler 的 try/catch
  • [ ] 将客户端密码存储在用户机密中

验证

  • [ ] dotnet build 成功
  • [ ] 在 Microsoft Entra 管理中心 中创建的应用注册
  • [ ] appsettings.json 具有真正的 GUID(无占位符)

测试和故障排除

完成实现后,运行应用程序并验证端到端身份验证流。

运行应用程序

启动 Aspire AppHost 以启动 Web 和 API 项目:

# From solution root
dotnet restore
dotnet build

# Launch AppHost (starts both Web and API)
dotnet run --project .\MyService.AppHost\MyService.AppHost.csproj

测试身份验证流

  1. 打开浏览器→ Blazor Web 界面(查看 Aspire 仪表板以获取 URL)。
  2. 选择 Login → 使用 Microsoft Entra 登录。
  3. 导航到 “天气 ”页。
  4. 验证天气数据加载(来自受保护的 API)。

解决常见问题

下表列出了常见问题及其解决方案:

問题 解决方案
API 调用时为 401 appsettings.json 验证与 API 的应用 ID URI 匹配的范围
OIDC 重定向失败 /signin-oidc 添加到 Microsoft Entra 重定向 URI
令牌未附加 确保在AddMicrosoftIdentityMessageHandler上调用HttpClient
服务发现失败 检查 AppHost.cs 引用这两个项目,并且它们正在运行
AADSTS65001 需要管理员同意 - 在Microsoft Entra 管理中心中授予许可
无登录按钮 确保 UserInfo.razor 存在且包含在 MainLayout.razor
许可循环 确保在所有调用 API 的页面上使用 try/catch HandleExceptionAsync

启用 MSAL 日志记录

排查身份验证问题时,请启用详细的 MSAL 日志记录,以便查看令牌获取的详细信息。 将以下日志级别添加到 appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Identity": "Debug",
      "Microsoft.IdentityModel": "Debug"
    }
  }
}

警告

禁用生产环境中的调试日志记录,因为日志记录可能会非常冗长。

检查令牌

若要调试令牌问题,请在 jwt.ms 解码 JWT 并验证:

  • aud (受众):匹配 API 的客户端 ID 或应用 ID URI
  • iss (颁发者):与租户匹配(https://login.microsoftonline.com/<tenant-id>/v2.0
  • scp (权限范围):包含所需的权限范围
  • exp (过期):令牌未过期

探索常见方案

以下部分演示如何扩展其他用例的基本实现。

保护 Blazor 页面

[Authorize] 属性添加到需要身份验证的页面:

@page "/weather"
@attribute [Authorize]

或在 Program.cs中定义授权策略。

// Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});
@attribute [Authorize(Policy = "AdminOnly")]

验证 API 中的作用域

通过链接 RequireScope确保 API 只接受具有特定范围的令牌。

app.MapGet("/weatherforecast", () =>
{
    // ... implementation
})
.RequireAuthorization()
.RequireScope("access_as_user");

使用仅限应用的令牌(服务到服务)

对于没有用户上下文的后台服务器场景或服务之间的调用,请将 `RequestAppToken` 设置为 `true`。

builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
    client.BaseAddress = new("https+http://apiservice");
})
.AddMicrosoftIdentityMessageHandler(options =>
{
    options.Scopes.Add("api://<api-client-id>/.default");
    options.RequestAppToken = true;
});

将无证书凭据用于生产

对于Azure中的生产部署,请使用托管标识而不是客户端机密。 配置ClientCredentials节如下所示:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-guid>",
    "ClientId":  "<web-app-client-id>",
    "ClientCredentials": [
      {
        "SourceType": "SignedAssertionFromManagedIdentity",
        "ManagedIdentityClientId": "<user-assigned-mi-client-id>"
      }
    ]
  }
}

有关详细信息,请参阅 无证书身份验证

从 API 调用下游 API(代表)

如果您的 API 需要代表用户调用另一个下游 API,请在 Program.cs 启用代用户令牌获取。

// MyService.ApiService/Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

builder.Services.AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi"));

将下游 API 配置添加到 appsettings.json

{
  "GraphApi": {
    "BaseUrl": "https://graph.microsoft.com/v1.0",
    "Scopes": [ "User.Read" ]
  }
}

然后从终结点调用下游 API:

{
    var user = await downstreamApi.GetForUserAsync<JsonElement>("GraphApi", "me");
    return user;
}).RequireAuthorization();

有关详细信息,请参阅 调用下游 API