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

.NET 7 Web API JWT授权异常:无法通过Token访问接口

.NET 7 Web API JWT认证故障:Token生成后无法访问接口(错误码从405转为401)

开发.NET 7 Web API时,登录接口可正常生成JWT Token,但使用该Token访问受保护接口时失败。参考相关方案调整后,错误码从405(方法不允许)变为401(未授权)。


相关代码片段

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("CORSPolicy", builder =>
    {
        builder
        .AllowAnyMethod()
        .AllowAnyHeader()
        .WithOrigins("http://localhost:3000", "https://localhost:3000", "https://appname.asurestaticapps.net");
    });
});

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("auth0", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey
    });

    options.OperationFilter<SecurityRequirementsOperationFilter>();
});

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

builder.Services.AddAuthorization(auth =>
{
    auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser().Build());
});

builder.Services.AddDbContext<SdeResearchDbContext>();
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<SdeResearchDbContext>()
    .AddDefaultTokenProviders();

builder.Services.AddScoped<JwtService>();

builder.Services.Configure<IdentityOptions>(options =>
{
    // 密码设置
    options.Password.RequireDigit= true;
    options.Password.RequireLowercase= true;
    options.Password.RequireNonAlphanumeric= true;
    options.Password.RequireUppercase= true;
    options.Password.RequiredLength= 8;
    options.Password.RequireLowercase= true;
    options.Password.RequiredUniqueChars = 6;

    // 用户设置
    options.User.AllowedUserNameCharacters =
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+!";
    options.User.RequireUniqueEmail= true;
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseCors("CORSPolicy");
app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "Topic",
    pattern: "topic/*{action}"
    );

app.Run();

appsettings.json

{
  "Jwt": {
    "Key": "this is the secret key for the jwt, it must be kept secure",
    "Issuer": "https://localhost:7174",
    "Audience": "https://localhost:7174",
    "Subject": "JWT for this site"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

JwtService.cs

namespace SdeResearch.Api.Services
{
    public class JwtService
    {
        private const int EXPIRATION_MINUTES = 5;

        private readonly IConfiguration _configuration;

        public JwtService(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public AuthenticationResponse CreateToken(ApplicationUser user)
        {
            var expiration = DateTime.UtcNow.AddMinutes(EXPIRATION_MINUTES);

            var token = CreateJwtToken(
                CreateClaims(user),
                CreateSigningCredentials(),
                expiration
                );

            var tokenHandler = new JwtSecurityTokenHandler();

            return new AuthenticationResponse
            {
                Token = tokenHandler.WriteToken(token),
                Expiration = expiration
            };
        }

        private JwtSecurityToken CreateJwtToken(Claim[] claims, SigningCredentials credentials, DateTime expiration) =>
             new JwtSecurityToken(
                 _configuration["Jwt:Issuer"],
                 _configuration["Jwt:Audience"],
                 claims,
                 expires: expiration,
                 signingCredentials: credentials
             );

        private Claim[] CreateClaims(ApplicationUser user) =>
            new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, _configuration["Jwt:Subject"]),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
                new Claim(ClaimTypes.NameIdentifier, user.Id),
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(ClaimTypes.Email, user.Email),
            };

        private SigningCredentials CreateSigningCredentials() =>
            new SigningCredentials(
                    new SymmetricSecurityKey(
                            Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])
                        ),
                    SecurityAlgorithms.HmacSha256
                );
    }
}

AuthController 登录方法

[HttpPost]
[Route("/account/login")]
public async Task<ActionResult<AuthenticationResponse>> Login(LoginDTO userLogin)
{
    ApplicationUser user = await _userManager.FindByEmailAsync(userLogin.Email);
    bool result = await _userManager.CheckPasswordAsync(user, userLogin.Password);
    if (!result)
        return BadRequest(result);

    var token = _jwtService.CreateToken(user);

    return Ok(token);
}

TopicController.cs

namespace SdeResearch.Api.Controllers
{
    [ApiController]
    [Route("[controller]")]
    [Authorize("Bearer")]
    public class TopicController : ControllerBase
    {
        private readonly SdeResearchDbContext _db;

        public TopicController(SdeResearchDbContext db)
        {
            _db = db;
        }

        [HttpGet]
        [Route("/topic/get-all-topics")]
        public async Task<List<Topic>> GetTopicsAsync()
        {
            return await _db.Topics.ToListAsync();
        }
    }
}

Postman测试截图

Postman端点测试截图


问题排查与修复方案

1. 认证参数配置矛盾

在Program.cs的JWT验证参数中,同时设置了ValidateIssuer = false、ValidateAudience = false和具体的ValidIssuer、ValidAudience,导致验证逻辑冲突。生成Token时已经指定了Issuer和Audience,验证阶段应该开启对应校验,确保一致性:

修改Program.cs中的认证配置:

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

2. 路由重复配置导致405错误

TopicController同时使用控制器级路由[Route("[controller]")]和方法级绝对路由[Route("/topic/get-all-topics")],加上Program.cs中额外配置的MapControllerRoute,导致路由匹配混乱,引发405错误。

  • 删除Program.cs中的以下路由配置:
// app.MapControllerRoute(
//     name: "Topic",
//     pattern: "topic/*{action}"
//     );
  • 修改TopicController的路由定义,避免绝对路由和前缀重复:
namespace SdeResearch.Api.Controllers
{
    [ApiController]
    [Route("topic")]
    [Authorize("Bearer")]
    public class TopicController : ControllerBase
    {
        private readonly SdeResearchDbContext _db;

        public TopicController(SdeResearchDbContext db)
        {
            _db = db;
        }

        [HttpGet("get-all-topics")]
        public async Task<List<Topic>> GetTopicsAsync()
        {
            return await _db.Topics.ToListAsync();
        }
    }
}

3. 请求头格式验证

确保请求时Authorization头的格式为Bearer {Token内容},缺少Bearer 前缀会直接导致401未授权。

4. Token过期检查

当前Token有效期仅5分钟,测试时需确保Token未过期,可临时延长有效期便于调试。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 01:52:18