.NET 8 Blazor Server交互式应用:为HttpClient附加Azure AD AccessToken
问题描述
开发.NET 8 交互式服务器端渲染Blazor应用,对接Azure租户完成身份认证,登录功能正常,但调用同租户下.NET API的HttpClient请求无法附加正确的AccessToken。
已通过获取电路中的认证状态解决作用域问题,但不知道如何从认证状态中提取AccessToken;直接在DelegatingHandler中使用ITokenAcquisition时触发异常:MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call。
相关代码如下:
Program.cs
using LogisticsTruckScales.BlazorServer.Components; using LogisticsTruckScales.BlazorServer.Services.Secuirty; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.Identity.Web; using MudBlazor.Services; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); builder.Services.AddMudServices(); builder.Services.AddScoped<CircuitServicesAccessor>(); builder.Services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>(); // Add services to the container builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); builder.Services.AddScoped<ScaleTransactionManagerAuthorizationHandler>(); builder.Services.AddHttpClient("ScaleTransactionManager", client => { client.BaseAddress = new Uri(builder.Configuration["ScaleTransactionManager:BaseUrl"]); }).AddHttpMessageHandler<ScaleTransactionManagerAuthorizationHandler>(); builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi() .AddInMemoryTokenCaches(); builder.Services.AddAuthorization(options => { options.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); }); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); app.MapRazorComponents<App>().AddInteractiveServerRenderMode(); app.Run();
ScaleTransactionManagerAuthorizationHandler.cs
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Identity.Client; using Microsoft.Identity.Web; using System.Net.Http.Headers; using System.Security.Claims; using static System.Formats.Asn1.AsnWriter; namespace LogisticsTruckScales.BlazorServer.Services.Secuirty { public class ScaleTransactionManagerAuthorizationHandler : DelegatingHandler { private readonly CircuitServicesAccessor circuitServicesAccessor; private readonly IConfiguration _configuration; public ScaleTransactionManagerAuthorizationHandler(CircuitServicesAccessor circuitServicesAccessor, IConfiguration configuration) { this.circuitServicesAccessor = circuitServicesAccessor; _configuration = configuration; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var authStateProvider = circuitServicesAccessor.Services .GetRequiredService<AuthenticationStateProvider>(); var authState = await authStateProvider.GetAuthenticationStateAsync(); //I have the auth state here. I want to pass the access token to the request. // Attach the token to the request request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); return await base.SendAsync(request, cancellationToken); } } }
CircuitServices相关代码
using Microsoft.AspNetCore.Components.Server.Circuits; namespace LogisticsTruckScales.BlazorServer.Services.Secuirty { public class CircuitServicesAccessor { static readonly AsyncLocal<IServiceProvider> blazorServices = new(); public IServiceProvider? Services { get => blazorServices.Value; set => blazorServices.Value = value; } } public class ServicesAccessorCircuitHandler( IServiceProvider services, CircuitServicesAccessor servicesAccessor) : CircuitHandler { public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler( Func<CircuitInboundActivityContext, Task> next) => async context => { servicesAccessor.Services = services; await next(context); servicesAccessor.Services = null; }; } public static class CircuitServicesServiceCollectionExtensions { public static IServiceCollection AddCircuitServicesAccessor( this IServiceCollection services) { services.AddScoped<CircuitServicesAccessor>(); services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>(); return services; } } }
解决方案
方案1:从ClaimsPrincipal直接提取AccessToken
利用Microsoft.Identity.Web提供的扩展方法,从认证状态的ClaimsPrincipal中直接获取已缓存的AccessToken:
修改ScaleTransactionManagerAuthorizationHandler的SendAsync方法:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var authStateProvider = circuitServicesAccessor.Services .GetRequiredService<AuthenticationStateProvider>(); var authState = await authStateProvider.GetAuthenticationStateAsync(); var user = authState.User; // 从配置中读取目标API的权限范围 var apiScope = _configuration["AzureAd:Scopes:ScaleTransactionManager"]; // 调用扩展方法获取AccessToken var accessToken = await user.GetAccessTokenForUserAsync(new[] { apiScope }); if (!string.IsNullOrEmpty(accessToken)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } return await base.SendAsync(request, cancellationToken); }
方案2:在电路作用域内正确使用ITokenAcquisition
异常原因是ITokenAcquisition未在正确的电路作用域中获取,需从电路的ServiceProvider中实例化ITokenAcquisition:
修改ScaleTransactionManagerAuthorizationHandler:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var circuitServices = circuitServicesAccessor.Services; if (circuitServices == null) { return await base.SendAsync(request, cancellationToken); } var authStateProvider = circuitServices.GetRequiredService<AuthenticationStateProvider>(); var authState = await authStateProvider.GetAuthenticationStateAsync(); var user = authState.User; var tokenAcquisition = circuitServices.GetRequiredService<ITokenAcquisition>(); var apiScope = _configuration["AzureAd:Scopes:ScaleTransactionManager"]; try { var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(new[] { apiScope }, user: user); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } catch (MsalUiRequiredException) { // 处理需要重新认证的情况,触发登录跳转 var navManager = circuitServices.GetRequiredService<NavigationManager>(); navManager.NavigateTo("/MicrosoftIdentity/Account/SignIn", forceLoad: true); return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized); } return await base.SendAsync(request, cancellationToken); }
配套配置说明
- appsettings.json中补充API范围配置:
"AzureAd": { "Instance": "https://login.microsoftonline.com/", "Domain": "你的租户域名", "TenantId": "你的租户ID", "ClientId": "Blazor应用的ClientId", "CallbackPath": "/signin-oidc", "Scopes": "openid profile offline_access", "ScaleTransactionManager": "api://你的API ClientId/access_as_user" }
- 注册认证服务时关联API范围:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi(new[] { builder.Configuration["AzureAd:ScaleTransactionManager"] }) .AddInMemoryTokenCaches();
- 验证CircuitServicesAccessor作用域:确保
ServicesAccessorCircuitHandler在每次电路活动时正确设置和清理服务实例,避免跨作用域的服务引用问题。
内容的提问来源于stack exchange,提问作者Roman
相关产品推荐
相关产品推荐

