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

如何基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 07:36:29