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

.NET Web API集成测试中JWT获取与使用异常排查

问题:从API响应获取JWT后用于集成测试请求返回401未授权

在基于JWT认证授权的.NET Web API集成测试中,调用/start-registration接口获取token后,携带该token请求/complete-registration时返回401未授权;但在测试项目中直接复制API的token生成代码生成的token,能正常通过授权。Postman单独测试各端点时认证授权机制符合预期,测试通过WebApplicationFactory在内存中运行API。

相关代码

生成token的控制器方法

// POST /start-registration
[HttpPost("start-registration")]
[AllowAnonymous]
public async Task<ActionResult<string>> StartRegistrationAsync([FromBody] RegistrationDto dto)
{
    try
    {
        if (validator.IsValid(dto))
        {
            var acc = await GetUnregisteredAccountAsync(dto);
            if(acc is not null)
            {
                if (acc.IsAuthenticated<RegistrationDto>(dto))
                {
                    if (acc.IsAuthorized())
                    {
                        await acc.RecordAuthnActivityAsync("Success");

                        return CreateRegistrationToken(config);
                    }
                    else
                    {
                        await acc.RecordAuthnActivityAsync("Failure");
                        return Unauthorized(UnauthorizedMessage());
                    }
                }
                else
                {
                    await acc.RecordAuthnActivityAsync("Failure");
                    return NotFound(NotFoundMessage());
                }
            }
            else
            {
                return NotFound(NotFoundMessage());
            }
        }
        else
        {
            return BadRequest(BadRequestMessage());
        }
    }
    catch (MySqlException ex)
    {
        exHandler.HandleException(ex, logger);
        return StatusCode(500);
    }
}

需要token验证的控制器方法

// POST /complete-registration
[HttpPost("complete-registration")]
[Authorize(Roles = REGISTRATION_ONLY)]
public async Task<ActionResult> CompleteRegistrationAsync([FromBody] RegistrationDto dto)
{
    try
    {
        if (validator.IsValid(dto))
        {
            await RegisterAccountAsync(dto);
            return Ok(OkMessage());
        }
        else
        {
            return BadRequest(BadRequestMessage());
        }
    }
    catch (MySqlException ex)
    {
        exHandler.HandleException(ex, logger);
        // Error code 1062 is thrown when a duplicate entry is attempted in a column
        // with a UNIQUE constraint.
        // Will occur if the desired username already exists.
        if(ex.Number == 1062)
        {
            return Conflict("Username already exists.");
        }
        else
        {
            return StatusCode(500);
        }
    }
}

API的token生成方法

public static string CreateRegistrationToken(IConfiguration config)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    var claims = new[]
    {
        new Claim(ClaimTypes.Role, "Registration Only")
     };

    var token = new JwtSecurityToken(config["Jwt:Issuer"], config["Jwt:Audience"], claims, expires: DateTime.Now.AddHours(1), signingCredentials: credentials);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

测试方法(后续将重构)

[Fact]
public async Task RegisterAccount_WithRegistrationSuccess_ReturnsOk()
{
    // Arrange
    NewAccountDto newAccdto = new()
    {
        FirstName = "Jane",
        LastName = "Doe",
        PhoneNumber = "1234567890",
        Location = "Some City",
        Position = "Crew",
        Shift = "Day",
        Status = "Active"
    };
    var response = await client.PostAsJsonAsync("/account/add-new", newAccdto);
    var registrationCode = await response.Content.ReadFromJsonAsync<int>();

    RegistrationDto regDto = new()
    {
        RegistrationStage = RegStage.START,
        FirstName = "Jane",
        LastName = "Doe",
        RegistrationCode = registrationCode
    };
    response = await client.PostAsJsonAsync("/account/start-registration", regDto);
    var token = await response.Content.ReadAsStringAsync();

    regDto = new()
    {
        RegistrationStage = RegStage.COMPLETE,
        FirstName = "Jane",
        LastName = "Doe",
        NewUsername = "NewUsername",
        NewPassword = "NewPassword"
    };

    // Act
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    response = await client.PostAsJsonAsync("/account/complete-registration", regDto);

    // Assert
    response.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
}

Program.cs中的认证授权配置

// Add JWT Authentication.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

// Add default authorization policy
builder.Services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder()
    .RequireClaim(ClaimTypes.Role, new string[] { "Crew", "Manager", "General Manager", "Administrator",
    "Recovery Only", "Registration Only"})
    .Build());

已尝试的方案

  • 修改StartRegistrationAsync返回类型为封装token的DTO,测试中通过ReadFromJsonAsync<DtoType>()获取token;
  • 在Program.cs中显式配置DefaultAuthenticationScheme和DefaultChallengeScheme;
  • 删除原有额外授权策略,将角色添加到默认授权策略;
  • 在测试项目中复制CreateRegistrationToken()方法直接生成token,此方法下测试通过。

排查方向与解决建议

  1. 检查token获取的格式问题
    用ReadAsStringAsync()获取token时,可能返回的内容带有额外引号(比如API返回JSON字符串而非纯token)。可以在测试中打印token值,若包含"或多余字符,用token = token.Trim('"')处理。

  2. 验证测试环境配置一致性
    确认集成测试项目的配置文件(如appsettings.Test.json)中Jwt:Issuer、Jwt:Audience、Jwt:Key与API运行时的配置完全一致,避免因配置差异导致token验证失败。

  3. 解析token对比声明内容
    在测试中获取token后,解析查看其声明是否符合预期,对比直接生成的token:

    var handler = new JwtSecurityTokenHandler();
    var jwtToken = handler.ReadJwtToken(token);
    // 打印Role声明、Issuer、Audience等信息,确认与直接生成的token一致
    
  4. 检查请求头格式
    确保Authorization头格式为Bearer + token,无多余空格。打印client.DefaultRequestHeaders.Authorization的值确认格式正确。

  5. 确认API响应的Content-Type
    若/start-registration响应的Content-Type为application/json,直接用ReadAsStringAsync()会拿到带引号的字符串,此时应改用ReadFromJsonAsync<string>()获取纯token值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 02:00:03