如何用OpenIddict实现基于声明的多租户?中间件死锁问题求助
问题描述
我自研了一个基于OpenIddict的授权服务器,用于学习和公司内部SSO方案POC,采用单租户单数据库架构,搭配Finbuckle.MultiTenant实现多租户管理:
- 租户元信息存储在专用PostgreSQL数据库
tenants中 - 用户登录表单包含
client id(即租户ID)、用户名和密码,提交时通过委托策略从表单读取租户信息,再针对对应租户数据库完成登录,租户连接字符串通过依赖注入注入到作用域DbContext - 用户登录后,
client id作为声明存入Cookie中
授权码流程中,/authorize端点运行正常,但访问/token端点时陷入死锁:
- 若
UseMultiTenant()在UseAuthentication()之前:此时身份声明尚未加载,无法获取租户信息 - 若顺序相反:
UseAuthentication()无法识别租户,无法连接对应数据库验证请求,导致验证失败
试过调整中间件顺序,也考虑过解码授权码注入租户信息,但不够优雅。希望找到让OpenIddict具备租户感知能力的最佳方案。
解决方案
方案1:从Token请求参数中提取租户ID(适配Finbuckle)
Finbuckle支持自定义租户解析策略,针对/token请求,可以直接从请求参数里提取client_id作为租户ID,无需依赖身份声明:
- 注册自定义租户解析策略:
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(...); // 保留原有租户存储配置
- 保持
UseMultiTenant()在UseAuthentication()之前:处理/token请求时,先通过参数拿到租户ID,后续认证就能用对应数据库完成验证。
方案2:扩展OpenIddict的认证前置逻辑
在OpenIddict认证流程前手动解析授权码中的租户信息,确保DbContext能正确注入:
- 添加自定义中间件,放在
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
相关产品推荐
相关产品推荐

