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

.NET 7 API用Specflow模拟JWT认证时始终返回Unauthorized的问题

问题描述

我有一个.NET 7 API,尝试使用Specflow/xUnit编写测试用例覆盖接口,但无法模拟API的认证逻辑,访问带[Authorize]特性的端点时始终返回Unauthorized状态码。

API核心配置(Program.cs)

public partial class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Configuration
        OidcConfigurationSection oidcConfig = new OidcConfigurationSection();
        builder.Configuration.GetSection(OidcConfigurationSection.SectionName).Bind(oidcConfig);

        // Authentication
        builder.Services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(options =>
        {
            //options.Authority = oidcConfig.Authority;
            //options.Audience = oidcConfig.Identifier;
            // Received token should have a user identified, the audience it asked for initially and the issuer should be the one that we asked for.
            options.TokenValidationParameters = new TokenValidationParameters()
            {
                NameClaimType = ClaimTypes.NameIdentifier,
                ValidAudience = oidcConfig.Identifier,
                ValidIssuer = oidcConfig.Authority
            };
        });

        ...

        var app = builder.Build();

        ...

        app.Run();
    }
}

受保护端点示例

[ApiController]
[Route("user/settings")]
[Authorize]
public class UserSettingsController : ControllerBase
{
    ...

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        return Ok(_dataRepo.Get())
    }
}

测试项目配置

FakeJwtManager类(生成模拟JWT)

public static class FakeJwtManager
{
    public static string Issuer { get; } = Guid.NewGuid().ToString();
    public static string Audience { get; } = Guid.NewGuid().ToString();
    public static SecurityKey SecurityKey { get; }
    public static SigningCredentials SigningCredentials { get; }

    private static readonly JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
    private static readonly RandomNumberGenerator generator = RandomNumberGenerator.Create();
    private static readonly byte[] key = new byte[32];

    static FakeJwtManager()
    {
        generator.GetBytes(key);
        SecurityKey = new SymmetricSecurityKey(key) { KeyId = Guid.NewGuid().ToString() };
        SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
    }

    public static string GenerateJwtToken()
    {
        return tokenHandler.WriteToken(new JwtSecurityToken(Issuer, Audience, null, null, DateTime.UtcNow.AddMinutes(10), SigningCredentials));
    }
}

FakeJwtWebApplicationFactory类(重写认证配置)

public class FakeJwtWebApplicationFactory<TStartup> : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
            {
                //options.Authority = FakeJwtManager.Issuer;
                //options.Audience = FakeJwtManager.Audience;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    IssuerSigningKey = FakeJwtManager.SecurityKey,
                    ValidIssuer = FakeJwtManager.Issuer,
                    ValidAudience = FakeJwtManager.Audience
                };
            });
        });
    }
}

Specflow步骤定义

[Binding]
public class UserCreationStepDefinitions
{
    private readonly FakeJwtWebApplicationFactory<Program> _webApplicationFactory;
    public HttpClient _httpClient { get; set; } = null!;
    private HttpResponseMessage _response { get; set; } = null!;

    public UserCreationStepDefinitions(FakeJwtWebApplicationFactory<Program> webApplicationFactory)
    {
        _webApplicationFactory = webApplicationFactory;
    }

    [Given(@"I am a client")]
    public void GivenIAmAClient()
    {
        _httpClient = _webApplicationFactory.CreateDefaultClient();
    }

    [Given(@"I provide valid authentified oidc user")]
    public void GivenIProvideValidAuthentifiedOidcUser()
    {
        var accessToken = FakeJwtManager.GenerateJwtToken();
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    }

    [When(@"I make a GET request to '([^']*)'")]
    public async Task WhenIMakeAGETRequestTo(string endpoint)
    {
        _response = await _httpClient.GetAsync($"{endpoint}");
    }

    [Then(@"The response status code is '([^']*)'")]
    public void ThenTheResponseStatusCodeIs(int statusCode)
    {
        var expected = (HttpStatusCode)statusCode;
        Assert.Equal(expected, _response.StatusCode);   <-- **Always false, expected is 'Unauthorized' instead of 'Ok'
    }
}
问题分析与解决方案

核心问题点

  1. 测试用TokenValidationParameters缺少NameClaimType配置
    原API的认证逻辑明确指定了NameClaimType = ClaimTypes.NameIdentifier,但测试项目的FakeJwtWebApplicationFactory中未同步该配置,导致JWT签名验证通过后,无法正确识别用户标识,最终触发Unauthorized。

  2. 生成的JWT令牌缺少用户标识Claim
    FakeJwtManager.GenerateJwtToken()方法生成的令牌未包含ClaimTypes.NameIdentifier类型的Claim,而原API要求以此作为用户唯一标识,因此即使签名验证通过,也会因缺少必要的用户信息而认证失败。

修复代码

1. 修复FakeJwtWebApplicationFactory

添加与原API一致的NameClaimType配置:

public class FakeJwtWebApplicationFactory<TStartup> : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    IssuerSigningKey = FakeJwtManager.SecurityKey,
                    ValidIssuer = FakeJwtManager.Issuer,
                    ValidAudience = FakeJwtManager.Audience,
                    // 新增:与原API保持一致的用户标识Claim类型
                    NameClaimType = ClaimTypes.NameIdentifier
                };
            });
        });
    }
}

2. 修复FakeJwtManager

生成令牌时添加NameIdentifier类型的Claim:

public static class FakeJwtManager
{
    // 现有代码保持不变...

    public static string GenerateJwtToken()
    {
        // 添加用户标识Claim
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())
        };

        return tokenHandler.WriteToken(new JwtSecurityToken(
            Issuer, 
            Audience, 
            claims, // 传入包含用户标识的Claim集合
            null, 
            DateTime.UtcNow.AddMinutes(10), 
            SigningCredentials));
    }
}

可选:彻底替换认证配置(若PostConfigure不生效)

如果上述修复后仍存在问题,可以直接移除原认证服务并重新配置:

public class FakeJwtWebApplicationFactory<TStartup> : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            // 移除原有的认证服务注册
            var authServiceDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IAuthenticationService));
            if (authServiceDescriptor != null)
            {
                services.Remove(authServiceDescriptor);
            }

            // 重新添加JWT认证,使用测试专用配置
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        IssuerSigningKey = FakeJwtManager.SecurityKey,
                        ValidIssuer = FakeJwtManager.Issuer,
                        ValidAudience = FakeJwtManager.Audience,
                        NameClaimType = ClaimTypes.NameIdentifier
                    };
                });
        });
    }
}

内容的提问来源于stack exchange,提问作者Nicolas G.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 15:44:55