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

请求提供基于子域名的OpenIddict+Finbuckle多租户认证基础示例

子域名租户+OpenIddict+ASP.NET Identity+Finbuckle多租户认证基础实现

以下是满足需求的最小可行实现方案,核心是通过Finbuckle识别子域名租户,结合OpenIddict实现租户级的认证管控:

1. 安装依赖包

在项目中安装以下NuGet包:

  • Finbuckle.MultiTenant.AspNetCore
  • OpenIddict.AspNetCore
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer(或其他你偏好的数据库提供者)

2. 配置子域名租户解析

2.1 配置租户存储(示例用配置文件)

在appsettings.json中添加租户配置,包含每个租户的OpenIddict客户端信息:

"Finbuckle:MultiTenant:Stores:ConfigurationStore": {
  "Tenants": [
    {
      "Id": "tenant1",
      "Identifier": "tenant1",
      "Name": "Tenant One",
      "Items": {
        "OpenIddict:ClientId": "tenant1-web-app",
        "OpenIddict:ClientSecret": "E5yXy8K8QvL2zR9tP3mN6bV0aS1dF4gH7jU2iO0p"
      }
    },
    {
      "Id": "tenant2",
      "Identifier": "tenant2",
      "Name": "Tenant Two",
      "Items": {
        "OpenIddict:ClientId": "tenant2-web-app",
        "OpenIddict:ClientSecret": "F4gH7jU2iO0pE5yXy8K8QvL2zR9tP3mN6bV0aS1d"
      }
    }
  ]
}

2.2 注册Finbuckle多租户服务

在Program.cs中配置子域名解析策略:

builder.Services.AddFinbuckleMultiTenant()
    .WithConfigurationStore() // 使用配置文件存储租户信息,生产环境可替换为数据库存储
    .WithHostStrategy(options =>
    {
        options.TryRemovePort = true;
        options.HostSeparator = '.';
        options.TenantIdentifierIndex = 0; // 取子域名的第一个部分(如tenant1.example.com → tenant1)
    });

3. 集成ASP.NET Identity

3.1 定义租户隔离的用户实体

创建自定义用户类,添加TenantId字段实现租户用户隔离:

public class ApplicationUser : IdentityUser
{
    public string TenantId { get; set; } = string.Empty;
}

3.2 配置DbContext与Identity服务

// 配置数据库上下文
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
    options.UseOpenIddict(); // 关联OpenIddict
});

// 配置Identity服务
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

在ApplicationDbContext中添加用户实体的索引配置:

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    // 为TenantId创建索引,优化查询
    builder.Entity<ApplicationUser>().HasIndex(u => u.TenantId);
}

4. 配置OpenIddict租户级认证

在Program.cs中注册OpenIddict服务,并添加租户验证逻辑:

builder.Services.AddOpenIddict()
    // 核心服务配置
    .AddCore(options =>
    {
        options.UseEntityFrameworkCore()
               .UseDbContext<ApplicationDbContext>();
    })
    // 服务器端配置
    .AddServer(options =>
    {
        // 配置端点
        options.SetAuthorizationEndpointUris("/connect/authorize")
               .SetTokenEndpointUris("/connect/token")
               .SetLogoutEndpointUris("/connect/logout");

        // 允许的授权流
        options.AllowAuthorizationCodeFlow()
               .AllowPasswordFlow()
               .AllowRefreshTokenFlow();

        // 添加租户验证逻辑:确保客户端ID与当前租户配置匹配
        options.AddEventHandler<OpenIddictServerEvents.ProcessAuthenticationContext>(builder =>
        {
            builder.UseInlineHandler(async context =>
            {
                var tenantContext = context.HttpContext.GetMultiTenantContext();
                if (tenantContext?.Tenant is null)
                {
                    context.Reject(
                        error: OpenIddictConstants.Errors.InvalidRequest,
                        description: "Invalid tenant identifier.");
                    return;
                }

                var expectedClientId = tenantContext.Tenant.Items["OpenIddict:ClientId"];
                if (string.IsNullOrEmpty(expectedClientId) || context.Request.ClientId != expectedClientId)
                {
                    context.Reject(
                        error: OpenIddictConstants.Errors.InvalidClient,
                        description: "Client ID does not match tenant configuration.");
                }
            });
        });

        // 开发环境用临时密钥,生产环境请替换为正式证书
        options.AddDevelopmentEncryptionCertificate()
               .AddDevelopmentSigningCertificate();

        // 关联ASP.NET Core管道
        options.UseAspNetCore()
               .EnableAuthorizationEndpointPassthrough()
               .EnableTokenEndpointPassthrough()
               .EnableLogoutEndpointPassthrough();
    })
    // 客户端配置
    .AddClient(options =>
    {
        options.UseAspNetCore()
               .EnableAuthorizationCodeFlowPassthrough()
               .EnableRefreshTokenFlowPassthrough();
    });

5. 配置中间件顺序(关键)

中间件顺序必须严格遵循:多租户中间件 → 认证中间件 → 授权中间件,否则租户上下文无法被认证逻辑获取:

var app = builder.Build();

// 其他基础中间件
app.UseHttpsRedirection();
app.UseStaticFiles();

// 多租户中间件必须在认证之前
app.UseMultiTenant();

app.UseRouting();

// 认证与授权中间件
app.UseAuthentication();
app.UseAuthorization();

// 路由映射
app.MapControllers();
app.MapRazorPages();

app.Run();

6. 租户级用户交互示例

6.1 注册页面关联租户

在注册页面的Post方法中,自动将当前租户ID关联到新用户:

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    var tenantContext = HttpContext.GetMultiTenantContext();
    
    if (tenantContext?.Tenant is null)
    {
        ModelState.AddModelError(string.Empty, "Invalid tenant.");
        return Page();
    }

    var user = new ApplicationUser 
    { 
        UserName = Input.Email, 
        Email = Input.Email, 
        TenantId = tenantContext.Tenant.Id 
    };
    
    var result = await _userManager.CreateAsync(user, Input.Password);
    if (result.Succeeded)
    {
        await _signInManager.SignInAsync(user, isPersistent: false);
        return LocalRedirect(returnUrl);
    }
    
    foreach (var error in result.Errors)
    {
        ModelState.AddModelError(string.Empty, error.Description);
    }
    return Page();
}

6.2 登录页面验证租户匹配

在登录页面的Post方法中,验证用户所属租户是否与当前请求租户一致:

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    var tenantContext = HttpContext.GetMultiTenantContext();
    
    if (tenantContext?.Tenant is null)
    {
        ModelState.AddModelError(string.Empty, "Invalid tenant.");
        return Page();
    }

    var user = await _userManager.FindByEmailAsync(Input.Email);
    if (user is null || user.TenantId != tenantContext.Tenant.Id)
    {
        ModelState.AddModelError(string.Empty, "Invalid login attempt.");
        return Page();
    }

    var result = await _signInManager.PasswordSignInAsync(user, Input.Password, Input.RememberMe, lockoutOnFailure: false);
    if (result.Succeeded)
    {
        return LocalRedirect(returnUrl);
    }
    // 其他失败逻辑...
    return Page();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 22:15:35