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

如何让ASP.NET Core JWT Bearer认证优先检查Authorization头而非Cookie

ASP.NET Core JWT认证问题:优先校验访问令牌并支持RBAC

背景说明

基于ASP.NET Core开发简单Web API,实现Refresh/Access Token认证机制,当前功能可用但不符合预期。

按设计,应用应优先校验请求头中的Access Token(访问令牌),以下是配置代码:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = builder.Configuration["JWT:Issuer"], // 确保与令牌的Issuer一致
        ValidateAudience = false, // 假设不校验受众,按需修改
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]!)),
        ValidateLifetime = true, // 启用有效期校验
        ClockSkew = TimeSpan.Zero // 可选:设为0消除时间偏移
    };

    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            // 检查是否存在"auth_token"Cookie并提取令牌
            if (context.Request.Cookies.ContainsKey("auth_token"))
            {
                context.Token = context.Request.Cookies["auth_token"];
            }
            return Task.CompletedTask;
        }
    };
});
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin"));
    options.AddPolicy("UserPolicy", policy => policy.RequireRole("User"));
});

安全逻辑设计:

  • 每次请求优先检查请求头中的Access Token(短有效期)
  • 前端(React)通过useEffect定时调用刷新端点获取新的Access Token,端点代码如下:
[HttpGet]
public async Task<IActionResult> RefreshAccessToken()
{
    // 从ControllerBase获取HttpContext,无需传参
    if (!HttpContext.Request.Cookies.ContainsKey("auth_token"))
        return StatusCode(401, "Unauthorized");

    var token = HttpContext.Request.Cookies["auth_token"];

    // 解码JWT提取声明
    var decodedClaims = _refreshTokenServices.DecodeJWT(token!);

    if (decodedClaims == null)
        return StatusCode(401, "Unauthorized");

    // 从解码后的JWT中获取用户名声明
    var usernameClaim = decodedClaims?.FindFirst(JwtRegisteredClaimNames.Sub);

    // 检查用户名声明是否存在
    if (usernameClaim == null)
        return StatusCode(401, "Unauthorized");

    var username = usernameClaim.Value;  // 获取实际用户名

    // 根据用户名查找用户
    var foundUser = await _userManager.FindByNameAsync(username);

    // 用户不存在则返回未授权
    if (foundUser == null)
        return StatusCode(401, "Unauthorized");

    // 为用户生成新的访问令牌
    var accessToken = _accessTokenServices.CreateToken(foundUser);

    // 返回新的访问令牌
    return Ok(new { AccessToken = accessToken.Result });
}
  • 前端为所有API请求添加重试逻辑:返回401时调用刷新令牌接口,返回403则直接返回错误,保障请求安全。

问题描述

当前配置完全忽略Bearer Token,仅校验HttpOnly Cookie中的Refresh Token(刷新令牌)。由于Refresh Token仅包含用户名和ID信息,无法支持基于角色的访问控制(RBAC)。只有删除Refresh Token后,应用才会检查Bearer Token。

希望通过特性(Attribute)保护路由,示例如下:

[HttpGet]
[Route("admin-test")]
[Authorize(Roles = "Admin")]
public IActionResult Admin()
{
    return Ok("Admin");
}

倾向于用特性实现,若无法实现也接受其他方案。

补充信息

登录接口返回Cookie代码

[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginRequestDto loginModel)
{
    if (!ModelState.IsValid) return BadRequest("Invalid Input Data");
    var user = await _userManager.Users.FirstOrDefaultAsync(u => u.UserName == loginModel.Username);
    if (user == null) return BadRequest("Wrong Username or Password");
    var passwordMatch = await _userManager.CheckPasswordAsync(user, loginModel.Password);
    if (!passwordMatch) return BadRequest("Wrong Username or Password");
    var refreshToken = _refreshToken.CreateToken(user);
    Response.Cookies.Append("auth_token", refreshToken, new CookieOptions()
    {
        HttpOnly = true,
        SameSite = SameSiteMode.None,
        Secure = false,  // 开发环境使用
        Expires = DateTime.Now.AddDays(7)
    });
    var accessToken = _accessToken.CreateToken(user);
    return Ok(new { AcessToken = accessToken.Result });
}

令牌生成代码

public async Task<string> CreateToken(Usuario usuario)
{
    var userRoles = await _userManager.GetRolesAsync(usuario);
    var authClaims = new List<Claim>()
    {
        new Claim(JwtRegisteredClaimNames.Sub, usuario.UserName!),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    };
    authClaims.AddRange(userRoles.Select(role => new Claim(ClaimTypes.Role, role)));

    // 确保过期时间为UTC,避免时区问题
    var expiresAt = DateTime.UtcNow.AddMinutes(15);

    var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature);

    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(authClaims),
        Expires = expiresAt, // 设置过期时间
        IssuedAt = DateTime.UtcNow, // 设置签发时间
        NotBefore = DateTime.UtcNow.AddSeconds(5), // 设置生效时间为签发后几秒
        SigningCredentials = creds,
        Issuer = _config["JWT:Issuer"]
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
}

Access Token与Refresh Token的区别仅在于有效期和声明:Access Token包含用户角色,Refresh Token仅包含用户基本信息。

最终解决方案

修改Program.cs中的逻辑,实现同时校验Access Token和Refresh Token,仅当两者均有效时通过认证,适配登出功能:

options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var authorizationHeader = context.Request.Headers["Authorization"].ToString();
        var httpOnlyCookie = context.Request.Cookies["auth_token"] ?? "";
        if (!string.IsNullOrEmpty(authorizationHeader) && authorizationHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
        {
            if (!string.IsNullOrEmpty(context.Request.Cookies["auth_token"]) && context.Request.Cookies.ContainsKey("auth_token"))
            {
                context.Token = authorizationHeader.Substring("Bearer ".Length).Trim();
            }
            else
            {
                context.Request.Headers["Authorization"] = "";
            }

        }
        return Task.CompletedTask;
    }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 08:39:54