You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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);
}

配套配置说明

  1. 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"
}
  1. 注册认证服务时关联API范围:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi(new[] { builder.Configuration["AzureAd:ScaleTransactionManager"] })
    .AddInMemoryTokenCaches();
  1. 验证CircuitServicesAccessor作用域:确保ServicesAccessorCircuitHandler在每次电路活动时正确设置和清理服务实例,避免跨作用域的服务引用问题。

内容的提问来源于stack exchange,提问作者Roman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 07:04:58