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

ASP.NET Core MVC 5:JWT刷新后AntiForgery引发400错误的解决咨询

问题背景与环境

ASP.NET Core MVC 5环境,采用Cookie认证,将JWT Token存入Claims实现登录验证。所有控制器均添加AutoValidateAntiForgeryToken特性处理XSRF问题。

JWT Token设置30分钟过期,在CookieAuthenticationEvents.ValidatePrincipal事件处理器中,Token过期时会刷新Token,并通过设置CookieValidatePrincipalContext.ShouldRenew = true重新生成认证Cookie,核心代码如下:

var identity = new ClaimsIdentity(context.Principal.Identity);
var sid = identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid);
identity.RemoveClaim(sid);
identity.AddClaim(new Claim(ClaimTypes.Sid, newJwtToken));
var newPrincipal = new ClaimsPrincipal(identity);
context.ReplacePrincipal(newPrincipal);
context.ShouldRenew = true;

问题现象

登录30分钟后,浏览器保存的AntiForgeryRequestToken失效,携带该Token发起POST请求会返回400 Bad Request。

补充测试情况

JWT Token过期前,携带AntiForgeryRequestToken的AJAX POST请求均可正常执行:

  • 登录20分钟后点击“更新”按钮,请求正常;
  • 停留在同一页面,21分钟后再次点击“更新”按钮,请求仍正常;
  • 重复操作至登录30分钟后点击按钮,触发400错误。

相关代码示例

cshtml代码

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@{
var xsrf = Xsrf.GetTokens(ViewContext.HttpContext); // GetAndStoreTokens are already called in a login page.
}

const ret = await $.post(
    '@Url.Action("Update")',
    {
        'id': 1234,
        'data1': "abc",
        '@(xsrf.FormFieldName)': '@(xsrf.RequestToken)'
    }
).promise();

ConfigureServices.cs

services.AddScoped<CookieAuthenticationEventHandler>();
services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(x => {
        x.LoginPath = "/Home";
        x.AccessDeniedPath = "/Home";
        x.LogoutPath = "/Signout";
        x.EventsType = typeof(CookieAuthenticationEventHandler);
        x.ExpireTimeSpan = timeout;
        x.SlidingExpiration = true; 
    });

SigninController.SignIn(signinModel)

/* Check ID and Password */
var jwtToken = Authorize(signinModel);
Claim[] claims = { new Claim(ClaimTypes.Sid, jwtToken), };
var claimsIdentity = new ClaimsIdentity(
    claims,
    CookieAuthenticationDefaults.AuthenticationScheme
);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
var timeout = config.GetValue<TimeSpan>("Timeout");
await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    claimsPrincipal
);
HttpContext.User = claimsPrincipal;

JWT Token生成代码(Authorize方法内)

public string CreateToken(IEnumerable<Claim> claims) {

    var timeStamp = DateTime.Now;
    var timeout = new TimeSpan("0.00:30:00");
    var token = new JwtSecurityToken(
        "https://unique-url",
        "https://unique-url",
        claims,
        timeStamp,
        expires: DateTime.Now.Add(timeout),
        signingCredentials: SigningCredentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

CookieAuthenticationEventHandler

public override async Task ValidatePrincipal(CookieValidatePrincipalContext context) {
    if(!await Validate(context)) {
        context.RejectPrincipal();
        await context.HttpContext.SignOutAsync();
    }
}

public async Task<bool> Validate(CookieValidatePrincipalContext context) {

    var jwtToken = context.Principal.Claims?.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
    var valid = await ValidatetokenAsync(jwtToken);

    if(valid) {
        return true;
    }

    var newJwtToken = await RefreshtokenAsync(jwtToken);
    if(string.IsNullOrEmpty(newJwtToken)) {
        return false;
    }

    var identity = new ClaimsIdentity(context.Principal.Identity);
    var sid = identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid);
    identity.RemoveClaim(sid);
    identity.AddClaim(new Claim(ClaimTypes.Sid, newJwtToken));
    var newPrincipal = new ClaimsPrincipal(identity);
    context.ReplacePrincipal(newPrincipal);
    context.ShouldRenew = true;

    return true;
}

根因分析

  1. 登录时传递给HttpContext.SignInAsync的ClaimsPrincipal包含JWT Token;
  2. 登录会话(Cookie)的过期时间与JWT Token的过期时间不同,Cookie开启滑动过期每次访问都会延长有效期,但JWT仅在过期时才刷新;
  3. JWT Token过期刷新后,HttpContext的ClaimsPrincipal被更新;
  4. AntiForgery验证默认会绑定ClaimsPrincipal的标识信息,当ClaimsPrincipal中的Claim(此处为存储JWT的Sid)发生变化时,之前生成的AntiForgery Token会失效;
  5. 因此JWT过期时发起POST请求会触发AntiForgery验证失败,返回400错误。

解决方案

方案一:调整AntiForgery验证依赖的标识(推荐)

AntiForgery默认会使用ClaimsPrincipal中的所有标识来生成验证Token,我们可以配置它仅依赖固定不变的用户标识(比如用户ID),而不是会随JWT刷新变化的Sid Claim。

在ConfigureServices中添加AntiForgery配置:

services.AddAntiforgery(options =>
{
    // 指定使用用户ID作为AntiForgery验证的标识依据,确保JWT刷新时该标识不变
    options.IdentityClaimType = ClaimTypes.NameIdentifier; // 需确保登录时已添加该Claim
});

同时,在登录时添加固定的用户ID Claim:

// 在SigninController.SignIn中,补充用户ID标识
Claim[] claims = { 
    new Claim(ClaimTypes.Sid, jwtToken),
    new Claim(ClaimTypes.NameIdentifier, signinModel.UserId) // 添加固定的用户ID
};

方案二:前端主动刷新AntiForgery Token

在JWT即将过期或刷新后,前端主动获取最新的AntiForgery Token,替换页面中存储的Token。

前端定时刷新逻辑:

// 每25分钟(提前JWT过期时间5分钟)获取新的AntiForgery Token
setInterval(async () => {
    const response = await fetch('@Url.Action("GetXsrfToken")', { credentials: 'include' });
    const data = await response.json();
    window.currentXsrfToken = data.requestToken;
}, 25 * 60 * 1000);

后端新增获取AntiForgery Token的接口:

[HttpGet]
public IActionResult GetXsrfToken()
{
    var tokens = _antiforgery.GetTokens(HttpContext);
    return Json(new { requestToken = tokens.RequestToken });
}

发起POST请求时使用最新Token:

const ret = await $.post(
    '@Url.Action("Update")',
    {
        'id': 1234,
        'data1': "abc",
        '@(xsrf.FormFieldName)': window.currentXsrfToken
    }
).promise();

方案三:同步JWT与Cookie的过期策略

将JWT的过期时间设置为与Cookie一致,并开启JWT的滑动刷新,每次请求时若JWT即将过期则自动刷新,避免用户在操作时突然触发Token过期。

添加JWT刷新ActionFilter:

public class JwtRefreshFilter : IActionFilter
{
    private readonly IConfiguration _config;

    public JwtRefreshFilter(IConfiguration config)
    {
        _config = config;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var jwtToken = context.HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value;
        if (!string.IsNullOrEmpty(jwtToken))
        {
            var handler = new JwtSecurityTokenHandler();
            var token = handler.ReadJwtToken(jwtToken);
            // 若Token剩余时间小于5分钟,则刷新
            if (token.ValidTo - DateTime.UtcNow < TimeSpan.FromMinutes(5))
            {
                var newJwtToken = RefreshtokenAsync(jwtToken).Result;
                // 更新ClaimsPrincipal与Cookie
                var identity = new ClaimsIdentity(context.HttpContext.User.Identity);
                var sid = identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid);
                identity.RemoveClaim(sid);
                identity.AddClaim(new Claim(ClaimTypes.Sid, newJwtToken));
                context.HttpContext.User = new ClaimsPrincipal(identity);
                context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, context.HttpContext.User).Wait();
            }
        }
    }

    public void OnActionExecuted(ActionExecutedContext context) { }
}

注册该Filter:

services.AddControllersWithViews(options =>
{
    options.Filters.Add<JwtRefreshFilter>();
});

诉求回应

当前实现的核心问题在于AntiForgery验证依赖了会随JWT刷新而变化的Claim,上述方案均可解决该问题。其中方案一从根源上隔离了AntiForgery验证与JWT的关联,是最稳定的解决方案;方案二和方案三则从前端或JWT刷新策略上规避了Token失效的场景,可根据实际业务需求选择。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 16:09:25