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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 03:29:52