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

ASP.NET Core服务:测试用硬编码公钥验证JWT,生产从身份提供商取公钥

解决方案推荐

针对你的需求,这里提供两种实用方案,既能复用生产环境的JWT认证配置,又能满足集成测试时自定义JWT的需求,同时保证测试覆盖生产代码。

方案一:抽象公钥获取逻辑,通过依赖注入切换环境实现

核心思路是定义一个公钥获取的抽象接口,生产环境从身份提供商的OIDC元数据自动拉取公钥,测试环境注入自定义公钥,复用同一套AddJwtBearer配置。

1. 定义公钥获取接口

public interface IPublicKeyProvider
{
    Task<IEnumerable<SecurityKey>> GetSigningKeysAsync(CancellationToken cancellationToken = default);
}

2. 生产环境实现(从OIDC元数据拉取公钥)

public class OidcPublicKeyProvider : IPublicKeyProvider
{
    private readonly string _authority;
    private readonly IHttpClientFactory _httpClientFactory;
    private IEnumerable<SecurityKey> _cachedKeys;
    private DateTime _cacheExpiry;

    public OidcPublicKeyProvider(string authority, IHttpClientFactory httpClientFactory)
    {
        _authority = authority;
        _httpClientFactory = httpClientFactory;
    }

    public async Task<IEnumerable<SecurityKey>> GetSigningKeysAsync(CancellationToken cancellationToken = default)
    {
        // 缓存公钥,避免频繁调用身份提供商接口
        if (_cachedKeys != null && DateTime.UtcNow < _cacheExpiry)
        {
            return _cachedKeys;
        }

        var httpClient = _httpClientFactory.CreateClient();
        // 获取OIDC元数据
        var metadataResponse = await httpClient.GetStringAsync($"{_authority.TrimEnd('/')}/.well-known/openid-configuration", cancellationToken);
        var metadata = JsonSerializer.Deserialize<OidcMetadata>(metadataResponse);

        // 获取JWKS公钥集合
        var jwksResponse = await httpClient.GetStringAsync(metadata.JwksUri, cancellationToken);
        var jwks = JsonWebKeySet.Create(jwksResponse);

        _cachedKeys = jwks.GetSigningKeys();
        _cacheExpiry = DateTime.UtcNow.AddHours(24); // 缓存24小时,可按需调整

        return _cachedKeys;
    }

    private class OidcMetadata
    {
        [JsonPropertyName("jwks_uri")]
        public string JwksUri { get; set; }
    }
}

3. 测试环境实现(注入自定义公钥)

public class TestPublicKeyProvider : IPublicKeyProvider
{
    private readonly SecurityKey _signingKey;

    public TestPublicKeyProvider(SecurityKey signingKey)
    {
        _signingKey = signingKey;
    }

    public Task<IEnumerable<SecurityKey>> GetSigningKeysAsync(CancellationToken cancellationToken = default)
    {
        return Task.FromResult(new[] { _signingKey }.AsEnumerable());
    }
}

4. 在Program.cs中配置依赖注入与JWT认证

var builder = WebApplication.CreateBuilder(args);

// 注册HttpClientFactory用于生产环境拉取公钥
builder.Services.AddHttpClient();

// 根据环境注册对应的公钥提供器
if (builder.Environment.IsProduction() || builder.Environment.IsStaging())
{
    var authority = builder.Configuration["Auth:Endpoint"];
    builder.Services.AddSingleton<IPublicKeyProvider>(sp => 
        new OidcPublicKeyProvider(authority, sp.GetRequiredService<IHttpClientFactory>()));
}
else if (builder.Environment.IsEnvironment("IntegrationTest"))
{
    // 测试环境注入自定义RSA公钥(可从测试配置读取或动态生成)
    var rsa = RSA.Create();
    rsa.ImportFromPem("-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----");
    var securityKey = new RsaSecurityKey(rsa);
    builder.Services.AddSingleton<IPublicKeyProvider>(new TestPublicKeyProvider(securityKey));
}

// 复用核心JWT认证配置
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
builder.Services
    .AddAuthentication(auth => {
        auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
        var authority = builder.Configuration["Auth:Endpoint"];
        var audience = builder.Configuration["JwtAuthentication:Audience"];

        options.Audience = audience;
        options.TokenValidationParameters = new TokenValidationParameters {
            RoleClaimType = "role",
            NameClaimType = "sub",
            ValidateIssuer = !builder.Environment.IsEnvironment("IntegrationTest"),
            ValidIssuer = builder.Environment.IsEnvironment("IntegrationTest") ? "test-issuer" : authority,
            ValidateAudience = true,
            ValidAudience = audience
        };

        // 动态加载签名密钥
        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = async context =>
            {
                var keyProvider = context.HttpContext.RequestServices.GetRequiredService<IPublicKeyProvider>();
                var signingKeys = await keyProvider.GetSigningKeysAsync();
                context.SecurityTokenHandler.Configuration.SigningKeys.Clear();
                foreach (var key in signingKeys)
                {
                    context.SecurityTokenHandler.Configuration.SigningKeys.Add(key);
                }
            }
        };

        // 生产环境保留Authority自动配置逻辑
        if (!builder.Environment.IsEnvironment("IntegrationTest"))
        {
            options.Authority = authority;
        }
    });

5. 集成测试中生成自定义JWT

public class IntegrationTests
{
    private TestServer _server;
    private HttpClient _client;
    private RSA _testRsa;

    [SetUp]
    public void Setup()
    {
        // 生成测试用RSA密钥对
        _testRsa = RSA.Create(2048);
        var testSigningKey = new RsaSecurityKey(_testRsa);

        var builder = new WebHostBuilder()
            .UseEnvironment("IntegrationTest")
            .ConfigureServices(services =>
            {
                // 替换为测试用公钥提供器
                services.AddSingleton<IPublicKeyProvider>(new TestPublicKeyProvider(testSigningKey));
            })
            .UseStartup<Startup>();

        _server = new TestServer(builder);
        _client = _server.CreateClient();
    }

    [Test]
    public async Task ProtectedEndpoint_ValidTestJwt_ReturnsOk()
    {
        // 生成自定义测试JWT
        var tokenHandler = new JwtSecurityTokenHandler();
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim("sub", "test-user"),
                new Claim("role", "admin")
            }),
            Expires = DateTime.UtcNow.AddHours(1),
            Issuer = "test-issuer",
            Audience = "your-audience",
            SigningCredentials = new SigningCredentials(new RsaSecurityKey(_testRsa), SecurityAlgorithms.RsaSha256)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        var jwt = tokenHandler.WriteToken(token);

        // 请求受保护端点
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
        var response = await _client.GetAsync("/api/protected");

        response.EnsureSuccessStatusCode();
        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    }
}

方案二:测试时启动内存版身份提供商

如果希望完全复用生产环境的Authority配置逻辑,可在测试中启动一个内存版的身份提供商(如IdentityServer4),让测试环境的认证流程与生产完全一致。

测试核心代码示例

public class IntegrationTests
{
    private TestServer _webServer;
    private HttpClient _webClient;

    [SetUp]
    public void Setup()
    {
        // 启动内存版IdentityServer
        var identityServerBuilder = new WebHostBuilder()
            .UseStartup<TestIdentityServerStartup>();
        var identityServer = new TestServer(identityServerBuilder);

        // 配置Web服务指向测试用IdentityServer
        var webBuilder = new WebHostBuilder()
            .UseEnvironment("IntegrationTest")
            .ConfigureAppConfiguration(config =>
            {
                config.AddInMemoryCollection(new Dictionary<string, string>
                {
                    ["Auth:Endpoint"] = identityServer.BaseAddress.ToString(),
                    ["JwtAuthentication:Audience"] = "your-audience"
                });
            })
            .UseStartup<Startup>();

        _webServer = new TestServer(webBuilder);
        _webClient = _webServer.CreateClient();
    }

    // 测试用IdentityServer启动配置
    public class TestIdentityServerStartup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            var rsa = RSA.Create(2048);
            var signingKey = new RsaSecurityKey(rsa);

            services.AddIdentityServer()
                .AddInMemoryApiScopes(new[] { new ApiScope("your-audience") })
                .AddInMemoryClients(new[] { 
                    new Client { 
                        ClientId = "test-client", 
                        AllowedGrantTypes = GrantTypes.ClientCredentials, 
                        ClientSecrets = { new Secret("test-secret".Sha256()) }, 
                        AllowedScopes = { "your-audience" } 
                    } 
                })
                .AddSigningCredential(signingKey);
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseIdentityServer();
        }
    }

    [Test]
    public async Task ProtectedEndpoint_ValidTokenFromTestIdp_ReturnsOk()
    {
        // 从测试IdentityServer获取Token
        var tokenResponse = await _webClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
        {
            Address = $"{_webServer.BaseAddress}connect/token",
            ClientId = "test-client",
            ClientSecret = "test-secret",
            Scope = "your-audience"
        });

        // 请求受保护端点
        _webClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
        var response = await _webClient.GetAsync("/api/protected");

        response.EnsureSuccessStatusCode();
        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    }
}

方案选择建议

  • 若需要完全控制JWT的内容和签名逻辑,优先选择方案一,配置灵活且无需额外启动服务。
  • 若希望测试流程与生产环境完全一致(包括从身份提供商获取公钥的逻辑),优先选择方案二,能覆盖完整的认证链路。

内容的提问来源于stack exchange,提问作者Claus Appel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 22:05:29