如何在OpenIddict中实现异步签名与加密密钥自动轮换?
OpenIddict异步密钥自动轮换解决方案
核心思路
OpenIddict默认依赖IOptions<OpenIddictServerOptions>同步读取密钥,且启动时强制校验密钥存在。要实现异步密钥轮换,核心是替换默认的密钥存储实现,绕开IOptions的同步限制,让OpenIddict从自定义的异步密钥存储中获取密钥,同时跳过启动阶段的密钥校验。
具体实现步骤
1. 跳过OpenIddict启动时的密钥校验
在配置OpenIddict服务时,禁用默认的密钥配置校验逻辑,避免启动时因无密钥报错:
services.AddOpenIddict() .AddServer(options => { // 禁用默认的密钥配置校验 options.DisableConfigurationValidation(); // 其他服务器配置(授权端点、令牌格式等) options.SetAuthorizationEndpointUris("/connect/authorize") .SetTokenEndpointUris("/connect/token") .AllowAuthorizationCodeFlow() .AllowRefreshTokenFlow(); // 不要调用AddDevelopmentEncryptionSigningKeys或AddSigningCertificate之类的默认密钥方法 });
2. 实现异步的密钥存储
自定义ISigningKeyStore和IEncryptionKeyStore(可继承OpenIddict提供的抽象基类简化实现),在里面处理异步的密钥加载、保存和轮换逻辑:
public class AsyncKeyStore : OpenIddictSigningKeyStore<OpenIddictKey>, IEncryptionKeyStore<OpenIddictKey> { private readonly IKeyRepository _keyRepository; // 自定义异步密钥存储仓库 private readonly ILogger<AsyncKeyStore> _logger; private IReadOnlyCollection<OpenIddictKey> _cachedKeys; private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); public AsyncKeyStore(IKeyRepository keyRepository, ILogger<AsyncKeyStore> logger) { _keyRepository = keyRepository; _logger = logger; } // 异步加载并缓存密钥 public override async ValueTask<IReadOnlyCollection<OpenIddictKey>> GetAllAsync(CancellationToken cancellationToken) { if (_cachedKeys != null) { return _cachedKeys; } await _lock.WaitAsync(cancellationToken); try { if (_cachedKeys != null) { return _cachedKeys; } // 从异步存储(数据库/分布式缓存等)加载密钥 var keys = await _keyRepository.GetAllKeysAsync(cancellationToken); _cachedKeys = keys.Select(k => new OpenIddictKey { Id = k.Id, Type = k.Type, Algorithm = k.Algorithm, Value = k.Value, CreatedAt = k.CreatedAt, ExpiresAt = k.ExpiresAt }).ToList(); return _cachedKeys; } finally { _lock.Release(); } } // 密钥轮换逻辑:生成新密钥并更新存储与缓存 public async Task RotateKeysAsync(CancellationToken cancellationToken) { await _lock.WaitAsync(cancellationToken); try { // 生成新的签名/加密密钥 var newSigningKey = GenerateNewSigningKey(); var newEncryptionKey = GenerateNewEncryptionKey(); // 异步保存新密钥到存储 await _keyRepository.SaveKeyAsync(newSigningKey, cancellationToken); await _keyRepository.SaveKeyAsync(newEncryptionKey, cancellationToken); // 清除缓存,下次查询时加载新密钥 _cachedKeys = null; } finally { _lock.Release(); } } // 实现单个密钥查询逻辑 public override ValueTask<OpenIddictKey> FindByIdAsync(string identifier, CancellationToken cancellationToken) { var key = _cachedKeys?.FirstOrDefault(k => k.Id == identifier) ?? await _keyRepository.GetKeyByIdAsync(identifier, cancellationToken); return new ValueTask<OpenIddictKey>(key); } // 生成RSA签名密钥 private OpenIddictKey GenerateNewSigningKey() { var rsa = RSA.Create(2048); return new OpenIddictKey { Id = Guid.NewGuid().ToString(), Type = OpenIddictConstants.KeyTypes.Rsa, Algorithm = OpenIddictConstants.Algorithms.RS256, Value = rsa.ToXmlString(includePrivateParameters: true), CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddDays(90) }; } // 生成AES加密密钥 private OpenIddictKey GenerateNewEncryptionKey() { var aes = Aes.Create(); aes.KeySize = 256; return new OpenIddictKey { Id = Guid.NewGuid().ToString(), Type = OpenIddictConstants.KeyTypes.Octet, Algorithm = OpenIddictConstants.Algorithms.A256GCM, Value = Convert.ToBase64String(aes.Key), CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddDays(30) }; } // 复用签名密钥存储逻辑实现加密密钥存储接口 ValueTask<IReadOnlyCollection<OpenIddictKey>> IEncryptionKeyStore<OpenIddictKey>.GetAllAsync(CancellationToken cancellationToken) => GetAllAsync(cancellationToken); ValueTask<OpenIddictKey> IEncryptionKeyStore<OpenIddictKey>.FindByIdAsync(string identifier, CancellationToken cancellationToken) => FindByIdAsync(identifier, cancellationToken); }
3. 注册自定义密钥存储服务
在DI容器中注册自定义的密钥存储,替换OpenIddict的默认实现:
services.AddScoped<ISigningKeyStore<OpenIddictKey>, AsyncKeyStore>(); services.AddScoped<IEncryptionKeyStore<OpenIddictKey>, AsyncKeyStore>(); // 注册自定义密钥仓库(示例用EF Core实现异步数据库操作) services.AddScoped<IKeyRepository, EfCoreKeyRepository>();
4. 处理密钥初始化与自动轮换
- 启动时初始化:在Program.cs中,启动后异步生成初始密钥(确保首次请求时有可用密钥):
var app = builder.Build(); // 异步初始化密钥 using (var scope = app.Services.CreateScope()) { var keyStore = scope.ServiceProvider.GetRequiredService<AsyncKeyStore>(); var existingKeys = await keyStore.GetAllAsync(CancellationToken.None); if (!existingKeys.Any()) { // 首次启动,生成初始密钥 await keyStore.RotateKeysAsync(CancellationToken.None); } } // 其他中间件配置 app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.Run();
- 自动轮换:用Hosted Service定期触发密钥轮换:
public class KeyRotationHostedService : BackgroundService { private readonly IServiceProvider _serviceProvider; public KeyRotationHostedService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var rotationInterval = TimeSpan.FromDays(7); // 每周轮换一次 while (!stoppingToken.IsCancellationRequested) { await Task.Delay(rotationInterval, stoppingToken); using (var scope = _serviceProvider.CreateScope()) { var keyStore = scope.ServiceProvider.GetRequiredService<AsyncKeyStore>(); await keyStore.RotateKeysAsync(stoppingToken); } } } } // 注册Hosted Service services.AddHostedService<KeyRotationHostedService>();
关键注意事项
- 缓存策略:密钥缓存要合理设置,避免频繁读取存储,同时在轮换时及时清除缓存。
- 密钥兼容性:轮换时保留旧密钥一段时间,确保已颁发的令牌能正常验证,直到旧密钥过期。
- 异常处理:在密钥加载和轮换过程中添加异常捕获,避免服务崩溃。
内容的提问来源于stack exchange,提问作者TGlatzer
相关产品推荐
相关产品推荐

