为基于IdentityServer4的AspNetCore API集成测试排障
先解答你的核心疑惑
客户端配置要点:
- 必须设置
ClientId和ClientSecrets(要和API配置里的客户端信息完全一致) AllowedGrantTypes:你用ClientCredentials是正确的,这个模式适合服务间调用的场景AllowedScopes:必须和你API的Audience以及IdentityServer注册的ApiResource名称完全匹配,否则token里的scope/aud字段不被API认可,直接返回UnauthorizedAccessTokenType = AccessTokenType.Jwt是正确的,确保颁发JWT格式的可验证tokenAllowOfflineAccess:客户端凭证模式不需要这个配置,可以直接去掉,因为该模式不会生成refresh token
- 必须设置
AllowedScopes的设置:必须和真实环境的范围(或测试环境中注册的
ApiResource名称)完全一致。比如你这里用"api1",那API的Audience配置、IdentityServer注册的ApiResource名称都得是"api1",三者统一才能通过验证。AddApiResources的参数:这非常重要!你需要传入和API的
Audience对应的ApiResource实例,比如new ApiResource("api1", "api1name")。这个名称会作为token里的aud(受众)和scope字段,API验证token时会严格检查这个值,不匹配就会拒绝请求。
你的测试代码里的关键问题及修正
1. WebHostBuilder重复使用的错误
你代码里先通过IdentityServerProxy使用了webHostBuilder,之后又用同一个builder创建fakeAuthServer,这会触发WebHostBuilder allows creation only of a single instance of WebHost异常。解决方法是先创建IdentityServer的TestServer,再用这个服务器的客户端去获取token,避免重复使用同一个builder。
2. 冗余代码问题
你写了两次创建fakeAuthServer的逻辑,直接删掉重复部分即可。
3. GET请求携带Body的不规范问题
你的登录接口用了GET方法却传了请求体,这不符合HTTP规范,登录接口通常用POST方法,记得同步调整测试代码里的请求方法。
修正后的完整测试代码
[Fact] public async Task Attempt_To_Test_InMemory_IdentityServer() { // 1. 配置IdentityServer的客户端和ApiResource var clientConfiguration = new ClientConfiguration("MyClient", "MySecret"); var client = new Client { ClientId = clientConfiguration.Id, ClientSecrets = new List<Secret> { new Secret(clientConfiguration.Secret.Sha256()) }, AllowedScopes = new[] { "api1" }, AllowedGrantTypes = new[] { GrantType.ClientCredentials }, AccessTokenType = AccessTokenType.Jwt // 移除不需要的AllowOfflineAccess }; // 2. 创建IdentityServer的TestServer(只创建一次) var identityServerWebHostBuilder = new IdentityServerWebHostBuilder() .AddClients(client) .AddApiResources(new ApiResource("api1", "api1name")); var identityServer = new TestServer(identityServerWebHostBuilder.CreateWebHostBuilder()); var identityServerClient = identityServer.CreateClient(); // 3. 直接向测试IdentityServer请求AccessToken var tokenRequest = new Dictionary<string, string> { {"grant_type", "client_credentials"}, {"client_id", clientConfiguration.Id}, {"client_secret", clientConfiguration.Secret}, {"scope", "api1"} }; var tokenResponse = await identityServerClient.PostAsync("/connect/token", new FormUrlEncodedContent(tokenRequest)); tokenResponse.EnsureSuccessStatusCode(); var tokenContent = await tokenResponse.Content.ReadAsAsync<TokenResponse>(); // 4. 配置API服务器,让其通过BackChannelHandler指向测试IdentityServer Startup.BackChannelHandler = identityServer.CreateHandler(); // 覆盖API的配置,确保Audience和IdentityServer的ApiResource一致 var apiWebHostBuilder = new WebHostBuilder() .UseStartup<Startup>() .ConfigureAppConfiguration((context, config) => { config.AddInMemoryCollection(new Dictionary<string, string> { {"IdentityServerAuthority", "https://localhost:5001"}, // 随便填,因为BackChannelHandler会指向测试服务器 {"IdentityServerAudience", "api1"} // 必须和ApiResource名称一致 }); }); var apiServer = new TestServer(apiWebHostBuilder); var apiClient = apiServer.CreateClient(); apiClient.SetBearerToken(tokenContent.AccessToken); // 5. 调整登录请求为POST(符合HTTP规范) var user = new User { Username = "simonlomax@ekm.com", Password = "Password-123" }; var req = new HttpRequestMessage(HttpMethod.Post, "/api/users/login") { Content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"), }; // Act var response = await apiClient.SendAsync(req); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); }
额外优化建议
- 为IdentityServer创建专门的测试Startup类,避免和生产代码耦合:
public class IdentityServerTestStartup { public void ConfigureServices(IServiceCollection services) { var client = new Client { ClientId = "MyClient", ClientSecrets = new List<Secret> { new Secret("MySecret".Sha256()) }, AllowedScopes = new[] { "api1" }, AllowedGrantTypes = new[] { GrantType.ClientCredentials }, AccessTokenType = AccessTokenType.Jwt }; services.AddIdentityServer() .AddInMemoryClients(new List<Client> { client }) .AddInMemoryApiResources(new List<ApiResource> { new ApiResource("api1", "api1name") }) .AddDeveloperSigningCredential(); // 测试用的临时签名证书 } public void Configure(IApplicationBuilder app) { app.UseIdentityServer(); } }
- 确保API控制器上的
[Authorize]特性没有指定额外的Policy,或者Policy的要求和token的scope完全匹配。
内容的提问来源于stack exchange,提问作者Simon Lomax
相关产品推荐
相关产品推荐

