Bearer认证下防伪造令牌用户不匹配问题及XSRF防护咨询
咱们先搞定那个防伪造令牌用户不匹配的错误,再把你的XSRF防护方案补得严严实实的。
这个错误的核心原因是防伪造令牌绑定的用户标识,和当前请求中认证用户的标识不一致。Antiforgery中间件默认会用用户的唯一标识(比如ClaimTypes.NameIdentifier或Identity.Name)关联令牌和用户,一旦两者对不上就会抛出这个错误。咱们从这几个点入手修复:
1. 给Identity添加稳定的用户唯一标识
你现在只添加了用户名声明,但用户名可能存在重复或变更的情况,而且Antiforgery优先认ClaimTypes.NameIdentifier作为用户的唯一标识。所以在GrantResourceOwnerCredentials里,务必补充这个声明:
// 假设你从数据库拿到了用户的唯一ID(比如user.Id) identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); // 保留你原来的用户名声明 identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName));
这样Antiforgery就能拿到稳定的用户标识,不会因为用户名的变化或匹配问题出错。
2. 确保XSRF令牌在用户认证后生成
如果你的XSRF令牌是在用户未登录时生成的,那这个令牌没有绑定任何用户标识,等用户登录后用它发起请求,就会出现“用户不匹配”的错误。所以必须在用户登录成功、认证状态生效之后,再生成并返回XSRF令牌。
3. 配置Antiforgery中间件对齐用户标识
在Startup里显式告诉Antiforgery中间件用哪个Claim作为用户标识,避免默认逻辑的歧义:
services.AddAntiforgery(options => { // 指定和你添加的Claim一致的类型 options.UserClaimType = ClaimTypes.NameIdentifier; // 配置XSRF令牌的Cookie属性(前端需要读取,所以不能设HttpOnly) options.Cookie.Name = "XSRF-TOKEN"; options.Cookie.HttpOnly = false; // 指定前端传递令牌的请求头名称 options.HeaderName = "X-XSRF-TOKEN"; // 生产环境务必开启Secure和Strict SameSite options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Strict; });
你的思路是对的:把Bearer令牌存在HttpOnly Cookie里防XSS,再配合XSRF令牌防CSRF。咱们把整个流程补全:
1. 安全存储Bearer令牌
在TokenEndpointResponse里,给Bearer令牌的Cookie加上严格的安全属性:
public override Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context) { var accessToken = context.AccessToken; context.Response.Cookies.Append("BearerToken", accessToken, new CookieOptions { HttpOnly = true, // 关键:防止前端JS读取,防XSS Secure = true, // 生产环境必须,只在HTTPS下传输 SameSite = SameSiteMode.Strict, // 阻止跨站发送Cookie Expires = DateTimeOffset.UtcNow.AddHours(1), // 和令牌过期时间保持一致 Path = "/" // 确保全站有效 }); return base.TokenEndpointResponse(context); }
2. 生成并传递XSRF令牌
- 后端提供XSRF令牌端点:创建一个需要认证的接口,专门返回XSRF令牌(确保令牌绑定当前用户):
[Authorize] [HttpGet("api/auth/xsrf-token")] public IActionResult GetXsrfToken([FromServices] IAntiforgery antiforgery) { var tokens = antiforgery.GetAndStoreTokens(HttpContext); // 把请求令牌存入Cookie,前端可以读取 Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions { HttpOnly = false, Secure = true, SameSite = SameSiteMode.Strict, Expires = DateTimeOffset.UtcNow.AddHours(1) }); return Ok(); } - 前端处理逻辑:用户登录成功后,调用这个端点获取XSRF令牌;之后每次发送POST/PUT/DELETE等状态变更请求时,从Cookie中读取
XSRF-TOKEN的值,放到请求头X-XSRF-TOKEN里。
3. 全局启用防伪造验证
在Startup里配置全局过滤,自动验证所有需要认证的状态变更请求:
services.AddMvc(options => { // 全局要求认证 var authPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); options.Filters.Add(new AuthorizeFilter(authPolicy)); // 自动验证防伪造令牌,针对POST/PUT/DELETE等方法 options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); });
如果有个别接口不需要验证,可以在接口上添加[IgnoreAntiforgeryToken]特性。
4. 额外安全加固
- 确保所有Cookie都设置
SameSite=Strict,减少跨站风险; - 生产环境强制开启HTTPS,配合
SecureCookie属性; - 定期轮换XSRF令牌,比如用户重新登录时生成新的令牌;
- 不要把XSRF令牌存在localStorage里,始终用Cookie存储(SameSite属性能提供更好的防护)。
如果还是出现错误,你可以在请求处理里打印这两个值,对比是否一致:
// 当前用户的标识 var userId = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; // 从请求中解析的XSRF令牌绑定的用户标识 var antiforgeryUserId = antiforgery.GetTokenAsync(HttpContext, "XSRF-TOKEN").Result?.Split(':')[0];
如果这两个值不一样,就说明令牌生成时的用户和当前请求的用户不是同一个,回到前面的步骤检查Identity配置和令牌生成时机。
内容的提问来源于stack exchange,提问作者DevEng

