IdentityServer4实现多配置数据库适配问题咨询
问题根因说明
你目前遇到的适配失效,本质是两个核心逻辑错位:
- IdentityServer4、ASP.NET Identity 相关的DbContext注册,在
ConfigureServices阶段就把固定值的连接字符串编译进了DbContextOptions实例,这个实例是单例缓存的,后续哪怕你修改全局IConfiguration里的连接字符串节点,已经生成的Options实例根本不会感知到变化。 - 你现在直接修改全局IConfiguration中
ConnectionStrings:default的实现本身存在严重并发隐患:全局IConfiguration是应用级单例,多并发请求修改同一个键值时会互相覆盖,出现跨租户串库的问题,哪怕适配了配置数据库,后续也必然出生产事故。
可落地的多数据库适配实现方案
完全可以实现IdentityServer4配置存储、操作存储的多数据库适配,不需要改IdentityServer的核心源码,只需要调整连接字符串的解析逻辑,把“启动时固定取值”改成“请求作用域内动态解析取值”,具体改造步骤如下:
1. 新增请求级别的租户上下文,替代全局配置修改
不要再修改全局IConfiguration的连接字符串值,新增Scoped生命周期的上下文类,专门存储当前请求对应的数据库连接信息:
// 请求隔离的租户上下文 public class TenantRequestContext { public string DatabaseKey { get; set; } public string ConnectionString { get; set; } }
在ConfigureServices中注册这个上下文:
services.AddScoped<TenantRequestContext>();
2. 改造现有数据库识别中间件
把原来修改全局配置的逻辑,改成给当前请求的租户上下文赋值,彻底避免并发串库的问题:
public async Task Invoke(HttpContext context, IConfiguration configuration, TenantRequestContext tenantContext) { var req = context.Request; if (!(req.Path == "/Account/Login" && req.Method.Equals("POST"))) { await _next(context); return; } if (!req.Form.Keys.Contains("Input.Database")) { throw new InvalidOperationException("No database key was sent with this request: " + req.Path); } var dbKey = req.Form["Input.Database"]; // 直接从配置读对应库的连接串,赋值给当前请求的上下文,不修改全局配置 tenantContext.DatabaseKey = dbKey; tenantContext.ConnectionString = configuration.GetConnectionString(dbKey); await _next(context); }
3. 调整所有相关DbContext的注册逻辑,支持动态连接字符串
不管是ASP.NET Identity的用户存储,还是IdentityServer的配置、操作存储,都要把原来“启动时固定传连接字符串”的写法,改成“从当前请求的租户上下文动态取连接字符串”:
- IdentityServer配置存储、操作存储的注册改造
var migrationsAssembly = typeof(Startup).Assembly.GetName().Name; var builder = services.AddIdentityServer(options => { // 原有IdentityServer的配置完全保留,不需要调整 options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = true; options.UserInteraction.LoginUrl = "/Account/Login"; options.UserInteraction.LogoutUrl = "/Account/Logout"; options.Authentication = new AuthenticationOptions() { CookieLifetime = TimeSpan.FromHours(10), CookieSlidingExpiration = true }; }) // 改造配置存储的DbContext初始化逻辑 .AddConfigurationStore(options => { options.ConfigureDbContext = dbBuilder => { // 从当前请求作用域拿租户上下文的连接串,没有的话兜底用默认连接串 var sp = dbBuilder.GetInfrastructure(); var tenantContext = sp.GetRequiredService<TenantRequestContext>(); var connString = tenantContext.ConnectionString ?? Configuration.GetConnectionString("default"); dbBuilder.UseSqlServer(connString, sql => sql.MigrationsAssembly(migrationsAssembly)); }; }) // 改造操作存储的DbContext初始化逻辑,和配置存储逻辑完全一致 .AddOperationalStore(options => { options.ConfigureDbContext = dbBuilder => { var sp = dbBuilder.GetInfrastructure(); var tenantContext = sp.GetRequiredService<TenantRequestContext>(); var connString = tenantContext.ConnectionString ?? Configuration.GetConnectionString("default"); dbBuilder.UseSqlServer(connString, sql => sql.MigrationsAssembly(migrationsAssembly)); }; }) .AddAspNetIdentity<ApplicationUser>() .AddProfileService<AspNetIdentityProfileService>();
- 原有的
IdentityUserDbContext如果之前也是靠改全局配置适配的,也需要按照同样的逻辑改造,把连接字符串的获取从固定值改成从TenantRequestContext动态取值,不要依赖全局配置的修改。
上线前需要注意的几个坑
- 中间件顺序:数据库识别中间件必须放在请求管道的最前端,早于IdentityServer认证中间件、MVC中间件,否则请求走到DbContext初始化阶段时,租户上下文还没有被赋值,会直接兜底走默认连接串。
- 登录后的请求适配:现在的逻辑只有登录POST请求会识别数据库key,登录成功后的后续请求(比如获取token、访问受保护接口),需要在登录成功时把
DatabaseKey作为自定义Claim写入认证Cookie,后续请求进来时从HttpContext.User中读取这个Claim,重新给TenantRequestContext赋值,不能只靠登录那次POST设置值。 - 数据库初始化:因为所有库的表结构是一致的,只需要用同一个迁移程序集,给每个目标数据库执行一次
dotnet ef database update初始化表结构即可,不需要单独维护多套迁移。
内容的提问来源于stack exchange,提问作者user7555530
相关产品推荐
相关产品推荐

