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

如何用OpenIddict实现基于声明的多租户?中间件死锁问题求助

问题描述

我自研了一个基于OpenIddict的授权服务器,用于学习和公司内部SSO方案POC,采用单租户单数据库架构,搭配Finbuckle.MultiTenant实现多租户管理:

  • 租户元信息存储在专用PostgreSQL数据库tenants中
  • 用户登录表单包含client id(即租户ID)、用户名和密码,提交时通过委托策略从表单读取租户信息,再针对对应租户数据库完成登录,租户连接字符串通过依赖注入注入到作用域DbContext
  • 用户登录后,client id作为声明存入Cookie中

授权码流程中,/authorize端点运行正常,但访问/token端点时陷入死锁:

  1. 若UseMultiTenant()在UseAuthentication()之前:此时身份声明尚未加载,无法获取租户信息
  2. 若顺序相反:UseAuthentication()无法识别租户,无法连接对应数据库验证请求,导致验证失败

试过调整中间件顺序,也考虑过解码授权码注入租户信息,但不够优雅。希望找到让OpenIddict具备租户感知能力的最佳方案。

解决方案

方案1:从Token请求参数中提取租户ID(适配Finbuckle)

Finbuckle支持自定义租户解析策略,针对/token请求,可以直接从请求参数里提取client_id作为租户ID,无需依赖身份声明:

  1. 注册自定义租户解析策略:
builder.Services.AddMultiTenant<TenantInfo>()
    .WithDelegateStrategy(context =>
    {
        // 针对token端点,直接从表单或查询参数取client_id
        if (context.Request.Path.StartsWithSegments("/connect/token"))
        {
            var clientId = context.Request.Form["client_id"].FirstOrDefault() ?? 
                           context.Request.Query["client_id"].FirstOrDefault();
            return Task.FromResult(clientId);
        }
        // 其他请求沿用原有逻辑(比如从Cookie声明取)
        var claimsPrincipal = context.User;
        return Task.FromResult(claimsPrincipal.FindFirst("client_id")?.Value);
    })
    .WithStore(...); // 保留原有租户存储配置
  1. 保持UseMultiTenant()在UseAuthentication()之前:处理/token请求时,先通过参数拿到租户ID,后续认证就能用对应数据库完成验证。

方案2:扩展OpenIddict的认证前置逻辑

在OpenIddict认证流程前手动解析授权码中的租户信息,确保DbContext能正确注入:

  1. 添加自定义中间件,放在UseMultiTenant()之后、UseAuthentication()之前:
app.UseMultiTenant();

// 自定义租户补全中间件
app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/connect/token") && 
        !context.GetMultiTenantContext().IsResolved)
    {
        var grantType = context.Request.Form["grant_type"].FirstOrDefault();
        if (grantType == "authorization_code")
        {
            var code = context.Request.Form["code"].FirstOrDefault();
            if (!string.IsNullOrEmpty(code))
            {
                // 复用OpenIddict服务解码授权码,提取租户声明
                var manager = context.RequestServices.GetRequiredService<IOpenIddictAuthorizationManager>();
                var authorization = await manager.FindByTokenAsync(code, OpenIddictConstants.TokenTypes.AuthorizationCode);
                if (authorization != null)
                {
                    var tenantId = authorization.GetClaim("client_id");
                    if (!string.IsNullOrEmpty(tenantId))
                    {
                        // 手动设置租户上下文
                        var tenantContext = context.GetMultiTenantContext();
                        tenantContext.TenantId = tenantId;
                        // 从存储加载租户信息(按需)
                        var store = context.RequestServices.GetRequiredService<IMultiTenantStore<TenantInfo>>();
                        tenantContext.TenantInfo = await store.TryGetByIdentifierAsync(tenantId);
                    }
                }
            }
        }
    }
    await next();
});

app.UseAuthentication();
app.UseAuthorization();

该方案复用OpenIddict现有逻辑,避免重复解码授权码,比手动解码更优雅。

方案3:让OpenIddict的DbContext支持动态租户连接

修改OpenIddict使用的DbContext,使其在构造时动态获取租户连接字符串,不依赖中间件顺序:

public class OpenIddictDbContext : DbContext
{
    private readonly IMultiTenantContext<TenantInfo> _tenantContext;

    public OpenIddictDbContext(DbContextOptions<OpenIddictDbContext> options, IMultiTenantContext<TenantInfo> tenantContext)
        : base(options)
    {
        _tenantContext = tenantContext;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (_tenantContext.TenantInfo != null)
        {
            optionsBuilder.UseNpgsql(_tenantContext.TenantInfo.ConnectionString);
        }
        base.OnConfiguring(optionsBuilder);
    }
}

保持UseMultiTenant()在UseAuthentication()之前,这样认证时需要访问数据库时,DbContext已能获取到租户的连接字符串。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 01:37:57