如何在.NET Core 9中实现Keycloak与xUnit集成测试?
解决Keycloak集成测试中Scheme重复的问题
问题根源
错误Scheme already exists: Bearer是因为测试代码中重复注册了认证方案:
- 原项目的
AddAuthenticationInternal已经注册了Bearer(JwtBearerDefaults.AuthenticationScheme)方案 - 测试的
ConfigureWebHost中又同时添加了AddJwtBearer和AddKeycloakWebApi(还指定了新的BearerTestscheme),导致冲突
修复步骤
1. 清理重复认证配置,重新指向Testcontainer Keycloak
修改测试Fixture的ConfigureWebHost方法,移除所有冗余的认证注册,只保留正确的Keycloak配置:
protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { // 移除现有DbContext配置(保留原有代码) var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<AppDbContext>)); if (descriptor != null) { services.Remove(descriptor); } // 彻底清理原有认证相关服务 services.RemoveAll(typeof(AuthenticationOptions)); services.RemoveAll(typeof(IAuthenticationSchemeProvider)); services.RemoveAll(typeof(IAuthenticationHandler)); // 使用测试容器的DbContext(保留原有代码) services.AddDbContext<AppDbContext>(options => options.UseSqlServer(_sqlServerContainer.GetConnectionString())); // 仅保留Keycloak认证配置,替换原有重复代码 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddKeycloakWebApi(options => { options.Realm = "my-realm"; options.AuthServerUrl = _keycloakContainer.GetBaseAddress(); options.SslRequired = "none"; options.Resource = "my-client"; options.VerifyTokenAudience = true; options.Audience = "account"; // 确保此secret与导入的my-realm.json中的客户端secret一致 options.Credentials = new KeycloakClientInstallationCredentials { Secret = "my-secret" }; }); }); }
2. 修正Keycloak Testcontainer启动配置
最新版Keycloak默认仅启用HTTPS,若你配置了ssl-required: none,需在容器启动时开启HTTP,并确保realm名称一致:
public async ValueTask InitializeAsync() { // 启动SQL Server容器(保留原有代码) _sqlServerContainer = new MsSqlBuilder() .WithImage("mcr.microsoft.com/mssql/server:2022-latest") .Build(); await _sqlServerContainer.StartAsync(); // 修改Keycloak容器配置 _keycloakContainer = new KeycloakBuilder() .WithImage("quay.io/keycloak/keycloak:latest") .WithName("keycloak_test") .WithResourceMapping("../../../../../keycloak-config/my-realm.json", "/opt/keycloak/data/import") // 使用start-dev命令,开启HTTP并导入realm .WithCommand("start-dev --import-realm --http-enabled true") // 映射HTTP端口8080(适配ssl-required: none配置) .WithPortBinding(8080, 8080) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") .WithUsername("admin") .WithPassword("admin") .WithCleanUp(true) .Build(); await _keycloakContainer.StartAsync(); }
3. 验证Realm配置一致性
确保:
- 导入的
my-realm.json中的realm名称为my-realm(与代码中options.Realm一致) - 客户端
my-client的secret与代码中options.Credentials.Secret完全匹配 - 测试容器的
GetBaseAddress()返回的是http://localhost:8080(对应映射的端口)
额外提示
若需要在测试中获取有效的JWT Token,可以通过Keycloak的API调用获取:
// 从测试容器的Keycloak获取Token var tokenClient = new HttpClient(); var tokenResponse = await tokenClient.PostAsync( $"{_keycloakContainer.GetBaseAddress()}/realms/my-realm/protocol/openid-connect/token", new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "password", ["client_id"] = "my-client", ["client_secret"] = "my-secret", ["username"] = "test-user", // 确保realm中存在此用户 ["password"] = "test-password" })); var tokenData = await tokenResponse.Content.ReadFromJsonAsync<JsonElement>(); var accessToken = tokenData.GetProperty("access_token").GetString();
在测试请求中添加Token:
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
内容的提问来源于stack exchange,提问作者BrePi
相关产品推荐
相关产品推荐

