如何在ASP.NET Core中间件中动态更新JWT Bearer配置?
问题背景
开发基于ASP.NET Core的JWT Bearer认证应用时,需要在不重启应用的前提下,动态更新JWT Bearer的Backchannel(用于JWT密钥获取、令牌验证等操作的HttpClient)。具体场景是应用启动后从Azure Key Vault下载代理密码,配置到Backchannel的代理中。
当前遇到的问题:
- 直接在
AddJwtBearer中使用services.BuildServiceProvider()获取IHttpClientFactory可以实现功能,但这是ASP.NET Core中的反模式,会导致服务容器重复构建、依赖注入生命周期混乱。 - 尝试在
JwtBearerEvents.OnMessageReceived事件中修改options.Backchannel,但修改无法被中间件识别,不生效。
反模式代码示例:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidIssuer = "YourIssuer", ValidAudience = "YourAudience", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("YourSecretKey")) }; // 反模式:提前构建服务容器 var serviceProvider = services.BuildServiceProvider(); var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>(); options.Backchannel = httpClientFactory.Create("Proxy"); });
无效的事件修改代码:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Events = new JwtBearerEvents { OnMessageReceived = context => { var httpClientFactory = context.HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>(); options.Backchannel = httpClientFactory.CreateClient("Proxy"); return Task.CompletedTask; } }; });
解决思路
1. 使用PostConfigureOptions延迟配置(避免反模式)
ASP.NET Core提供PostConfigureOptions机制,允许在所有服务配置完成后,再对选项进行最终配置。此时可以安全地从服务容器中获取依赖,无需提前构建服务容器。
代码示例:
// 先注册JWT认证基础配置 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidIssuer = "YourIssuer", ValidAudience = "YourAudience", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("YourSecretKey")) }; }); // 延迟配置Backchannel,此时服务容器已就绪 services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, (options, serviceProvider) => { var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>(); options.Backchannel = httpClientFactory.CreateClient("Proxy"); });
2. 运行时动态更新Backchannel(支持代理密码变化)
如果需要在应用运行时动态更新Backchannel的代理配置(比如Azure Key Vault中的密码更新),可以结合IOptionsMonitor监听配置变化,通过两种方式实现:
方式A:通过HttpClient的DelegatingHandler动态应用最新配置
定义代理配置类,绑定Azure Key Vault中的配置(需开启配置动态刷新):
public class ProxySettings { public string ProxyPassword { get; set; } public string ProxyUrl { get; set; } public int ProxyPort { get; set; } public string ProxyUsername { get; set; } }
配置HttpClientFactory,使用DelegatingHandler在每次请求时获取最新代理配置:
// 绑定ProxySettings到配置(Azure Key Vault配置提供器需提前配置) services.AddOptions<ProxySettings>() .BindConfiguration("ProxySettings") .ValidateDataAnnotations(); // 注册带动态代理的HttpClient services.AddHttpClient("Proxy") .AddHttpMessageHandler(sp => { var proxySettingsMonitor = sp.GetRequiredService<IOptionsMonitor<ProxySettings>>(); return new DynamicProxyHandler(proxySettingsMonitor); }); // 自定义DelegatingHandler,动态设置代理 public class DynamicProxyHandler : DelegatingHandler { private readonly IOptionsMonitor<ProxySettings> _proxySettingsMonitor; public DynamicProxyHandler(IOptionsMonitor<ProxySettings> proxySettingsMonitor) { _proxySettingsMonitor = proxySettingsMonitor; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 每次请求前获取最新的代理配置 var settings = _proxySettingsMonitor.CurrentValue; var proxy = new WebProxy(new Uri($"{settings.ProxyUrl}:{settings.ProxyPort}")) { Credentials = new NetworkCredential(settings.ProxyUsername, settings.ProxyPassword) }; request.Proxy = proxy; return await base.SendAsync(request, cancellationToken); } }
最后通过PostConfigureOptions将该HttpClient设置为JWT的Backchannel:
services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, (options, sp) => { var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>(); options.Backchannel = httpClientFactory.CreateClient("Proxy"); });
方式B:通过IOptionsMonitor直接更新JwtBearerOptions
当代理配置变化时,通过IOptionsMonitor更新JWT选项的Backchannel:
// 注册配置变更监听服务 services.AddHostedService<ProxySettingsUpdateService>(); public class ProxySettingsUpdateService : BackgroundService { private readonly IOptionsMonitor<ProxySettings> _proxySettingsMonitor; private readonly IOptionsMonitor<JwtBearerOptions> _jwtOptionsMonitor; private readonly IHttpClientFactory _httpClientFactory; public ProxySettingsUpdateService(IOptionsMonitor<ProxySettings> proxySettingsMonitor, IOptionsMonitor<JwtBearerOptions> jwtOptionsMonitor, IHttpClientFactory httpClientFactory) { _proxySettingsMonitor = proxySettingsMonitor; _jwtOptionsMonitor = jwtOptionsMonitor; _httpClientFactory = httpClientFactory; } protected override Task ExecuteAsync(CancellationToken stoppingToken) { // 监听ProxySettings变化 _proxySettingsMonitor.OnChange(settings => { // 更新JWT的Backchannel _jwtOptionsMonitor.Update(JwtBearerDefaults.AuthenticationScheme, options => { options.Backchannel = _httpClientFactory.CreateClient("Proxy"); }); }); return Task.CompletedTask; } }
为什么之前的事件修改不生效?
JwtBearerMiddleware内部使用IOptionsMonitor<JwtBearerOptions>来获取配置,但在OnMessageReceived事件中直接修改的options对象是启动时的快照副本,并非中间件实时使用的配置实例。正确的做法是通过IOptionsMonitor的更新机制来修改配置,或者让Backchannel的HttpClient自身处理动态配置。
内容的提问来源于stack exchange,提问作者scattered

