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

.NET 8中JWT生成正常但添加[Authorize]后返回404问题

.NET 8 JWT授权返回404问题解决方案

问题核心原因

  1. 密钥编码不一致:JWT验证时用Encoding.ASCII解析密钥,生成Token时用Encoding.UTF8,导致签名验证失败,触发默认挑战行为(重定向到登录页),但API无此页面,返回404。
  2. 角色声明类型不匹配:验证配置中指定RoleClaimType = "role",但生成Token时使用标准的ClaimTypes.Role(对应URI格式的声明类型),导致角色无法被识别,授权失败后触发404。
  3. 默认挑战行为未适配API:认证/授权失败时,框架默认重定向到登录页面,API场景下无对应路由,返回404而非标准401/403。
  4. TokenValidationParameters配置矛盾:ValidateLifetime = true但RequireExpirationTime = false,导致验证逻辑混乱。

修复步骤

1. 统一密钥编码方式

确保生成Token和验证Token时使用相同的编码格式,统一用UTF8(适配含非ASCII字符的密钥)。

2. 对齐角色声明类型

将JWT验证配置中的RoleClaimType改为标准的ClaimTypes.Role,与生成Token时的声明类型保持一致。

3. 配置API友好的挑战行为

修改JwtBearer事件,在认证失败或需要挑战时直接返回401/403 JSON响应,避免重定向。

4. 修正TokenValidationParameters配置

将RequireExpirationTime设为true,与ValidateLifetime = true的配置匹配。

5. 优化中间件顺序

显式添加UseRouting(),确保路由逻辑在认证授权之前执行;调整自定义异常中间件到最外层,统一处理错误响应。

修复后的代码

Program.cs 关键修改

using System.Text.Json;
// ... 其他using语句 ...

builder.Services.AddAuthentication(options => 
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt => 
{
    // 统一用UTF8解析密钥,和生成Token时一致
    var key = Encoding.UTF8.GetBytes(builder.Configuration.GetSection("JwtConfig:Secret").Value);

    jwt.SaveToken = true;
    jwt.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateLifetime = true,
        RequireExpirationTime = true, // 修正为true,匹配ValidateLifetime
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = true,
        ValidIssuer = builder.Configuration.GetSection("JwtConfig:Issuer").Value,
        ValidateAudience = true,
        ValidAudience = builder.Configuration.GetSection("JwtConfig:Audience").Value,
        RoleClaimType = ClaimTypes.Role // 改为标准的角色声明类型
    };
    jwt.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            Console.WriteLine($"Authentication failed: {context.Exception.Message}");
            // 直接返回401,不重定向
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            context.Response.ContentType = "application/json";
            return context.Response.WriteAsync(JsonSerializer.Serialize(new { 
                Status = "Failed", 
                Message = "Authentication failed: " + context.Exception.Message 
            }));
        },
        OnChallenge = context =>
        {
            // 拦截默认挑战行为,返回401 JSON
            context.HandleResponse();
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            context.Response.ContentType = "application/json";
            return context.Response.WriteAsync(JsonSerializer.Serialize(new { 
                Status = "Failed", 
                Message = "You are not authorized to access this resource" 
            }));
        },
        OnForbidden = context =>
        {
            // 处理无权限场景,返回403
            context.Response.StatusCode = StatusCodes.Status403Forbidden;
            context.Response.ContentType = "application/json";
            return context.Response.WriteAsync(JsonSerializer.Serialize(new { 
                Status = "Failed", 
                Message = "You do not have permission to access this resource" 
            }));
        }
    };
});

// ... 其他服务配置 ...

var app = builder.Build();

// 中间件顺序调整
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

// 自定义异常处理中间件(放在最外层)
app.Use(async (context, next) =>
{
    try
    {
        await next.Invoke();
        // 处理404情况,返回JSON响应
        if (context.Response.StatusCode == StatusCodes.Status404NotFound)
        {
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(JsonSerializer.Serialize(new { 
                Status = "Failed", 
                Message = "Resource not found" 
            }));
        }
    }
    catch (Exception e)
    {
        Console.WriteLine($"An error occurred: {e.Message}");
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(JsonSerializer.Serialize(new { 
            Status = "Failed", 
            Message = "An unexpected error occurred: " + e.Message 
        }));
    }
});

// 显式添加路由中间件
app.UseRouting();

// 认证授权必须在路由之后,端点之前
app.UseAuthentication();
app.UseAuthorization();

// 映射端点
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

// ... 角色和用户初始化代码 ...

AccountController 关键确认

确保生成Token时的密钥编码与验证一致(已为UTF8,无需修改):

private string GenerateJwtToken(User user)
{
    try
    {
        var JwtTokenHandler = new JwtSecurityTokenHandler();
        // 保持和验证时一致的UTF8编码
        var key = Encoding.UTF8.GetBytes(_configuration.GetSection("JwtConfig:Secret").Value);

        // ... 其他代码 ...
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error generating JWT token");
        throw;
    }
}

AdminController 启用角色授权

[Route("api/[controller]")]
[ApiController]
[Authorize(Roles = "Overseer")] // 启用角色授权
public class AdminController(UserManager<User> userManager) : ControllerBase
{
    // ... 现有代码 ...
}

验证方法

  1. 登录获取Token:调用POST /api/Account/Login,传入测试用户凭据(TimmyTurner/Password1!),获取JWT Token。
  2. 访问受保护路由:在请求头中添加Authorization: Bearer <你的Token>,调用GET /api/Admin/Dashboard。
    • 若Token有效且用户有对应角色,返回Welcome to the Dashboard。
    • 若Token无效/过期,返回401 JSON响应。
    • 若用户无对应角色,返回403 JSON响应。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 16:02:33