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

为基于IdentityServer4的AspNetCore API集成测试排障

解决IdentityServer4集成测试返回Unauthorized的问题

先解答你的核心疑惑

  • 客户端配置要点:

    • 必须设置ClientId和ClientSecrets(要和API配置里的客户端信息完全一致)
    • AllowedGrantTypes:你用ClientCredentials是正确的,这个模式适合服务间调用的场景
    • AllowedScopes:必须和你API的Audience以及IdentityServer注册的ApiResource名称完全匹配,否则token里的scope/aud字段不被API认可,直接返回Unauthorized
    • AccessTokenType = AccessTokenType.Jwt是正确的,确保颁发JWT格式的可验证token
    • AllowOfflineAccess:客户端凭证模式不需要这个配置,可以直接去掉,因为该模式不会生成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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:17:01