本指南演示如何使用 Microsoft Entra ID 身份验证和授权,保护 .NET Aspire 分布式应用程序。 它涵盖:
-
Blazor 服务器前端 (
MyService.Web):使用 OpenID Connect 和令牌获取用户登录 -
受保护的 API 后端(
MyService.ApiService):使用 Microsoft.Identity.Web 进行 JWT 验证 - 端到端流程:Blazor 获取访问令牌,然后通过 Aspire 服务发现来调用受保护的 API
本指南假定你已使用以下命令创建 Aspire 项目:
aspire new aspire-starter --name MyService
先决条件
- .NET 9 SDK 或更高版本
- .NET Aspire CLI - 请参阅 Install Aspire CLI
- Microsoft Entra 租户 — 请参阅 在 Microsoft Entra ID 中注册应用 以获取设置信息
小窍门
您是 Aspire 的新用户吗? 请参阅 .NET Aspire 概述。
了解两阶段工作流
本指南遵循两个阶段的方法:
| 阶段 | 发生的情况 | 结果 |
|---|---|---|
| 阶段 1 | 添加含占位符值的认证代码 | 应用程序能够构建,但无法运行 |
| 阶段 2 | 配置 Microsoft Entra 应用注册 | 应用使用实际身份验证运行 |
在 Microsoft Entra ID 中注册应用
在应用对用户进行身份验证之前,需要在Microsoft Entra中注册两个应用:
| 应用注册 | Purpose | 键配置 |
|---|---|---|
API (MyService.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>(用于Audiences和Scopes) - Web 应用客户端 ID — Web 应用注册的应用程序(客户端)ID
- 客户端密码 (或证书) - Web 应用的凭据(存储在用户机密中,而不是 appsettings.json)
-
范围 — 您的 Web 应用程序请求的范围,例如
api://<api-client-id>/.default或api://<api-client-id>/access_as_user
步骤 1:注册 API
- 转到 Microsoft Entra 管理中心>身份>应用程序>应用注册。
- 选择新注册。
-
名称:
MyService.ApiService - 支持的帐户类型: 仅限此组织目录中的帐户(单租户)
- 选择注册。
-
名称:
- 转到公开 API>在应用程序 ID URI 旁边添加。
- 接受默认值(
api://<client-id>)或对其进行自定义。 - 选择 “添加范围”
-
范围名称:
access_as_user - 谁可以同意: 管理员和用户
- 管理员同意显示名称: 访问 MyService API
- 管理员同意说明: 允许应用代表已登录用户访问 MyService API。
- 选择添加作用域。
-
范围名称:
- 接受默认值(
- 复制 应用程序(客户端)ID,因为你需要在两个
appsettings.json文件中使用它。
有关详细信息,请参阅 快速入门:配置应用以公开 Web API。
步骤 2:注册 Web 应用
- 转到 应用注册>新注册。
-
名称:
MyService.Web - 支持的帐户类型: 仅限此组织目录中的帐户
-
重定向 URI: 选择 Web 并输入应用的 URL +
/signin-oidc- 本地开发时:
https://localhost:7001/signin-oidc(请检查launchSettings.json的实际端口)
- 本地开发时:
- 选择注册。
-
名称:
- 转到 “身份验证”>添加 URI 以添加您所有的开发 URI(
launchSettings.json)。 - 转到 证书和机密>客户端机密>“新建客户端机密”。
- 添加说明和过期时间。
- 立即复制机密值 - 它不会再次显示。
- 转到 API 权限>,添加权限>,然后选择我的 API。
- 选择
MyService.ApiService。 - 选择“
access_as_user>添加权限”。 - 为 [租户] 选择“授予管理员同意”选项(或者在首次使用时将提示用户)。
- 选择
- 复制 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 组件 ”。
注释
BlazorAuthenticationChallengeHandler 和 LoginLogoutEndpointRouteBuilderExtensions 在 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
- 用户访问 Blazor 应用 →未通过身份验证→看到“登录”按钮。
-
用户选择登录 → 重定向到
/authentication/login→ OIDC 验证挑战 → Microsoft Entra。 -
用户登录 → Microsoft Entra 重定向
/signin-oidc→ cookie 已建立。 -
用户导航到天气页面 → Blazor 调用
WeatherApiClient.GetAsync()。 -
MicrosoftIdentityMessageHandler截获请求,从缓存获取令牌(或以无提示方式刷新),并附加Authorization: Bearer <token>标头。 - API 接收请求 → Microsoft。Identity.Web 验证 JWT →返回数据。
- 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 组件,则用户无法登录。
创建 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
替换 RouteView 为 AuthorizeRouteView 在 Components/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}";
}
}
}
}
该模式的工作原理如下:
-
IsAuthenticatedAsync()在进行 API 调用之前,检查用户是否已登录。 -
HandleExceptionAsync()catchesMicrosoftIdentityWebChallengeUserException(或作为 InnerException)。 - 如果是身份验证挑战异常,则用户将被重定向以重新使用所需的范围或声明进行身份验证。
- 如果这不是质询异常,则
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.json和AzureAd节更新了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
测试身份验证流
- 打开浏览器→ Blazor Web 界面(查看 Aspire 仪表板以获取 URL)。
- 选择 Login → 使用 Microsoft Entra 登录。
- 导航到 “天气 ”页。
- 验证天气数据加载(来自受保护的 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。
相关内容
- 快速入门:在 Web 应用中登录用户
- 快速入门:保护 Web API
- 调用自定义 API
- 凭据概述
- Microsoft 标识平台
- .NET Aspire概述
- 生成第一个 Aspire 应用
- 在 .NET Aspire 中的服务发现