如何基于Duende Identity Server搭建多租户(独立数据库)集中式身份服务器
基于Duende Identity Server实现独立数据库多租户身份认证
核心思路
Duende Identity Server本身支持多租户扩展,针对独立数据库租户的核心是实现「租户上下文动态切换」+「各租户数据完全隔离」,以下是分步实现方案:
1. 确定租户识别机制
首先需要明确如何从请求中识别租户,常用方案:
- 域名识别:比如
tenant1.your-idp.com、tenant2.your-idp.com(推荐,无侵入) - 请求头/路径参数:比如请求头携带
X-Tenant-ID,或路径前缀/tenant1/connect/token
以域名识别为例,实现租户解析逻辑:
public interface ITenantResolver { Task<Tenant> ResolveAsync(HttpRequest request); } public class DomainTenantResolver : ITenantResolver { // 中央配置库:存储所有租户的基础信息(ID、域名、数据库连接串等) private readonly ICentralTenantStore _centralStore; public DomainTenantResolver(ICentralTenantStore centralStore) { _centralStore = centralStore; } public async Task<Tenant> ResolveAsync(HttpRequest request) { var host = request.Host.Host; return await _centralStore.GetTenantByDomainAsync(host); } }
2. 注入租户上下文到请求管道
通过中间件将当前租户信息绑定到请求上下文,供后续服务使用:
public class TenantMiddleware { private readonly RequestDelegate _next; public TenantMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, ITenantResolver tenantResolver) { var tenant = await tenantResolver.ResolveAsync(context.Request); if (tenant == null) { context.Response.StatusCode = StatusCodes.Status404NotFound; return; } // 存储租户上下文,用Scoped服务更规范 context.Items["CurrentTenant"] = tenant; await _next(context); } } // Startup/Program.cs中注册中间件(需放在Identity Server中间件之前) app.UseMiddleware<TenantMiddleware>();
3. 实现动态切换租户数据库的用户存储
每个租户有独立的用户数据库,需基于当前租户上下文动态创建DbContext:
// 定义Scoped的租户上下文服务 public interface ITenantContext { Tenant CurrentTenant { get; } } public class TenantContext : ITenantContext { public Tenant CurrentTenant { get; } public TenantContext(IHttpContextAccessor httpContextAccessor) { CurrentTenant = httpContextAccessor.HttpContext?.Items["CurrentTenant"] as Tenant; } } // 动态创建租户DbContext的工厂 public class TenantDbContextFactory : IDbContextFactory<ApplicationDbContext> { private readonly ITenantContext _tenantContext; public TenantDbContextFactory(ITenantContext tenantContext) { _tenantContext = tenantContext; } public ApplicationDbContext CreateDbContext() { var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>(); optionsBuilder.UseSqlServer(_tenantContext.CurrentTenant.ConnectionString); return new ApplicationDbContext(optionsBuilder.Options); } } // 自定义用户Store,绑定当前租户的数据库 public class TenantUserStore : IUserStore<ApplicationUser> { private readonly ApplicationDbContext _dbContext; public TenantUserStore(IDbContextFactory<ApplicationDbContext> dbContextFactory) { _dbContext = dbContextFactory.CreateDbContext(); } // 实现IUserStore的所有方法,基于当前租户的_dbContext操作数据 public async Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken) { _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(cancellationToken); return IdentityResult.Success; } // 其他方法省略... }
4. 适配Duende Identity Server核心服务
替换Duende默认的用户验证和Profile服务,使其基于当前租户数据库操作:
// 自定义资源所有者密码验证器(密码模式用) public class TenantResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private readonly ITenantUserManager _userManager; public TenantResourceOwnerPasswordValidator(ITenantUserManager userManager) { _userManager = userManager; } public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { var user = await _userManager.FindByNameAsync(context.UserName); if (user == null || !await _userManager.CheckPasswordAsync(user, context.Password)) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant); return; } context.Result = new GrantValidationResult(user.Id.ToString(), "password"); } } // 自定义Profile服务 public class TenantProfileService : IProfileService { private readonly ITenantUserManager _userManager; public TenantProfileService(ITenantUserManager userManager) { _userManager = userManager; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var user = await _userManager.GetUserAsync(context.Subject); var claims = await _userManager.GetClaimsAsync(user); context.IssuedClaims.AddRange(claims); } public async Task IsActiveAsync(IsActiveContext context) { var user = await _userManager.GetUserAsync(context.Subject); context.IsActive = user != null && user.IsActive; } }
5. 注册多租户服务到DI容器
在Program.cs中完成所有服务的注册:
// 注册租户相关服务 builder.Services.AddScoped<ITenantContext, TenantContext>(); builder.Services.AddScoped<ITenantResolver, DomainTenantResolver>(); builder.Services.AddScoped<IDbContextFactory<ApplicationDbContext>, TenantDbContextFactory>(); builder.Services.AddScoped<IUserStore<ApplicationUser>, TenantUserStore>(); builder.Services.AddScoped<ITenantUserManager, TenantUserManager>(); // 替换Duende的核心服务 builder.Services.AddIdentityServer() .AddAspNetIdentity<ApplicationUser>() .AddResourceOwnerValidator<TenantResourceOwnerPasswordValidator>() .AddProfileService<TenantProfileService>();
关键注意事项
- 租户配置隔离:中央配置库仅存储租户基础信息(ID、域名、连接串),不存储用户/客户端数据;若租户需独立配置客户端/资源,需自定义
IClientStore和IResourceStore实现多租户适配。 - 数据库初始化:租户创建时自动执行EF Core迁移,初始化用户数据库表结构。
- 数据安全:严格校验租户上下文,禁止跨租户数据库访问;日志需携带租户ID,便于问题排查。
- 缓存策略:对租户配置、客户端信息等做缓存,减少数据库查询,但需在租户信息更新时主动刷新缓存。
内容的提问来源于stack exchange,提问作者trax
相关产品推荐
相关产品推荐

