如何让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
相关产品推荐
相关产品推荐

