.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实现:
- 在Program.cs中添加相关配置:
builder.Services.AddAuthorization(options => { // 创建默认策略,要求用户已认证 options.DefaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); }); // 注册自定义认证方案选择器 builder.Services.AddSingleton<IAuthenticationSchemeProvider, TenantAuthenticationSchemeProvider>(); // 注册HttpContextAccessor用于获取请求上下文 builder.Services.AddHttpContextAccessor();
- 实现
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
相关产品推荐
相关产品推荐

