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

.NET 7 Web API配置多AD身份认证报错:Scheme已存在

.NET API 配置多个Azure AD身份提供者的问题

我正在构建一个多租户SaaS应用,需要将多个Azure AD作为应用的身份验证器。单独配置一个AD时完全正常,但初始化多个MicrosoftIdentity提供者时出现System.InvalidOperationException: 'Scheme already exists: Bearer'错误。

我用单例TenantProvider加载所有租户的AD配置,模拟实现如下:

public class TenantProvider
{
    private static TenantProvider _instance;
    private static readonly object _lock = new object();

    public List<Tenant> tenants { get; set; }

    private TenantProvider() 
    { 
        tenants = new List<Tenant>();

        //mock1
        tenants.Add(new Tenant
        {
            instance = "https://login.microsoftonline.com/",
            domain = "MyDomain",
            tenantId = "MyTenantId",
            clientId = "MyClientId",
            callbackPath = "/signin-oidc",
            allowedHosts = "*"
        });

        //mock2
        tenants.Add(new Tenant
        {
            instance = "https://login.microsoftonline.com/",
            domain = "MyDomain2",
            tenantId = "MyTenantId2",
            clientId = "MyClientId2",
            callbackPath = "/signin-oidc",
            allowedHosts = "*"
        });
    }

    public static TenantProvider Instance()
    {
        lock (_lock)
        {
            if (_instance == null)
            {
                _instance = new TenantProvider();
            }
        }
        
        return _instance;
    }
}

然后在Program.cs中循环添加身份认证:

foreach(var tenantProvider in TenantProvider.Instance().tenants)
{
    Action<JwtBearerOptions> configureJwtBearerOptions = JWTBearerOptions;
    void JWTBearerOptions(JwtBearerOptions t1)
    {
        t1.Audience = $"{tenantProvider.instance}/{tenantProvider.tenantId}";
        t1.TokenValidationParameters.ValidAudiences = new string[] {
            tenantProvider.clientId,
            $"api://{tenantProvider.clientId}"
        };
    }

    Action<MicrosoftIdentityOptions> configureMicrosoftIdentityOptions = MIOptions;
    void MIOptions(MicrosoftIdentityOptions t2)
    {
        t2.TenantId = tenantProvider.tenantId;
        t2.ClientId = tenantProvider.clientId;
        t2.Instance = tenantProvider.instance;
        t2.Domain = tenantProvider.domain;
    }

    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                  .AddMicrosoftIdentityWebApi(configureJwtBearerOptions,
                                              configureMicrosoftIdentityOptions);
}

请问如何在.NET API中实现多个Azure AD身份提供者(非B2C)?


解决方案

错误核心原因是每次调用AddMicrosoftIdentityWebApi时都使用了默认的Bearer认证方案,重复注册同名方案导致冲突。要配置多个AD提供者,需要为每个租户指定唯一的认证方案,并实现动态选择方案的逻辑。

步骤1:为每个租户注册唯一认证方案

修改Program.cs中的循环逻辑,为每个租户生成唯一的方案名称(比如用租户ID作为后缀),并在注册时指定该方案:

// 先初始化认证服务,不指定默认方案
builder.Services.AddAuthentication();

foreach(var tenant in TenantProvider.Instance().tenants)
{
    // 为每个租户创建唯一的认证方案名称
    var schemeName = $"Bearer-{tenant.tenantId}";

    Action<JwtBearerOptions> configureJwtBearerOptions = options =>
    {
        options.Audience = $"{tenant.instance}/{tenant.tenantId}";
        options.TokenValidationParameters.ValidAudiences = new string[] {
            tenant.clientId,
            $"api://{tenant.clientId}"
        };
        // 可选:验证发行方,确保令牌来自对应租户的AD
        options.TokenValidationParameters.ValidIssuer = $"{tenant.instance}{tenant.tenantId}/v2.0";
    };

    Action<MicrosoftIdentityOptions> configureMicrosoftIdentityOptions = options =>
    {
        options.TenantId = tenant.tenantId;
        options.ClientId = tenant.clientId;
        options.Instance = tenant.instance;
        options.Domain = tenant.domain;
    };

    // 注册带唯一方案名称的Microsoft Identity认证服务
    builder.Services.AddAuthentication()
                  .AddMicrosoftIdentityWebApi(schemeName, 
                                              configureJwtBearerOptions,
                                              configureMicrosoftIdentityOptions);
}

步骤2:实现动态认证方案选择逻辑

现在有多个认证方案,需要在请求到达时动态判断使用哪个租户的方案。这里通过自定义IAuthenticationSchemeProvider实现:

  1. 在Program.cs中添加相关配置:
builder.Services.AddAuthorization(options =>
{
    // 创建默认策略,要求用户已认证
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

// 注册自定义认证方案选择器
builder.Services.AddSingleton<IAuthenticationSchemeProvider, TenantAuthenticationSchemeProvider>();
// 注册HttpContextAccessor用于获取请求上下文
builder.Services.AddHttpContextAccessor();
  1. 实现TenantAuthenticationSchemeProvider类:
public class TenantAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
    private readonly TenantProvider _tenantProvider;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, 
                                              TenantProvider tenantProvider,
                                              IHttpContextAccessor httpContextAccessor) 
        : base(options)
    {
        _tenantProvider = tenantProvider;
        _httpContextAccessor = httpContextAccessor;
    }

    public override async Task<AuthenticationScheme?> GetDefaultAuthenticateSchemeAsync()
    {
        var context = _httpContextAccessor.HttpContext;
        if (context == null) return await base.GetDefaultAuthenticateSchemeAsync();

        // 示例:从请求头"X-Tenant-Id"获取租户ID,可根据业务调整为域名、路径参数等
        var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
        if (string.IsNullOrEmpty(tenantId)) return null;

        // 根据租户ID匹配对应的认证方案
        var schemeName = $"Bearer-{tenantId}";
        return await GetSchemeAsync(schemeName);
    }
}

步骤3:可选:通过令牌自动识别租户

如果不想让客户端传递租户ID头,可以解析令牌的iss(发行方)字段自动匹配租户,修改GetDefaultAuthenticateSchemeAsync方法:

public override async Task<AuthenticationScheme?> GetDefaultAuthenticateSchemeAsync()
{
    var context = _httpContextAccessor.HttpContext;
    if (context == null) return await base.GetDefaultAuthenticateSchemeAsync();

    // 从Authorization头提取令牌
    var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
    if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) return null;

    var token = authHeader.Substring("Bearer ".Length);
    try
    {
        // 解析令牌的发行方
        var handler = new JwtSecurityTokenHandler();
        var jwtToken = handler.ReadJwtToken(token);
        var issuer = jwtToken.Issuer;

        // 匹配对应的租户
        var tenant = _tenantProvider.tenants.FirstOrDefault(t => 
            $"{t.instance}{t.tenantId}/v2.0" == issuer || 
            $"{t.instance}{t.tenantId}" == issuer);

        if (tenant == null) return null;

        var schemeName = $"Bearer-{tenant.tenantId}";
        return await GetSchemeAsync(schemeName);
    }
    catch
    {
        return null;
    }
}

关键注意事项

  • 每个租户的认证方案名称必须唯一,避免重复注册冲突。
  • 动态选择方案的逻辑需贴合业务场景(如域名、请求头、路径参数等)。
  • 确保每个租户的Token验证参数(Audience、Issuer)配置准确,防止跨租户令牌被误验证。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 14:17:55