请求提供基于子域名的OpenIddict+Finbuckle多租户认证基础示例
子域名租户+OpenIddict+ASP.NET Identity+Finbuckle多租户认证基础实现
以下是满足需求的最小可行实现方案,核心是通过Finbuckle识别子域名租户,结合OpenIddict实现租户级的认证管控:
1. 安装依赖包
在项目中安装以下NuGet包:
Finbuckle.MultiTenant.AspNetCoreOpenIddict.AspNetCoreMicrosoft.AspNetCore.Identity.EntityFrameworkCoreMicrosoft.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
相关产品推荐
相关产品推荐

