显然随机错误: "防伪令牌验证失败。防伪 cookie 令牌和请求令牌不匹配。"

7

背景

我有一个相对较新的ASP.NET Core 2网站。它运行在一台服务器上(Windows Server 2012 R2,IIS 8.5),每当我上传更新时,我只会在几天后重新启动该网站。大约每天都会由于反伪造系统的拒绝而导致用户请求失败。这些是POST请求,并没有什么特别之处。我在POST请求中包含了反伪造值,99%的时间,POST请求都能正常工作。但是当它们不起作用时,stdout日志会显示“Antiforgery token validation failed. The antiforgery cookie token and request token do not match.” 当我使用该确切语句进行Web搜索时,我得到零结果。因此,我转向Stack Overflow寻求帮助。[现在这个问题已经不再是真实的,因为Web搜索现在可以得到这个Stack Overflow问题。]

错误

下面是stdout日志的相关部分。

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
      Request starting HTTP/1.1 POST [domain redacted] application/x-www-form-urlencoded 234
info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter[1]
      Antiforgery token validation failed. The antiforgery cookie token and request token do not match.
Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The antiforgery cookie token and request token do not match.
   at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet)
   at Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery.<ValidateRequestAsync>d__9.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ValidateAntiforgeryTokenAuthorizationFilter.<OnAuthorizationAsync>d__3.MoveNext()
info: Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker[3]
      Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.AutoValidateAntiforgeryTokenAuthorizationFilter'.
info: Microsoft.AspNetCore.Mvc.StatusCodeResult[1]
      Executing HttpStatusCodeResult, setting HTTP status code 400
info: Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker[2]
      Executed action /Index in 2.6224ms
warn: Microsoft.AspNetCore.Antiforgery.Internal.DefaultAntiforgery[1]
      Antiforgery validation failed with message 'The antiforgery cookie token and request token do not match.'.

对于产生上述标准输出的请求,IAntiforgery.IsRequestValidAsync 返回 false。请注意错误消息“防伪令牌 cookie 和请求令牌不匹配。”以下是一个失败的 POST 请求和相关 cookie 的简化示例。

Cookie: .AspNetCore.Antiforgery.ClRyCRmWApY=CfDJ8F9Fs4CqDFpLttT96eZw9WFtJht41WcNrmgshi2pFGwcxhr0_0hvINQc7Yl9Cbjhv-TiSNXeEctyKborLI49AcjHfWIgOmmKkbjOe7QMn8Z0WZtkQy5JcaBHKEGTu1p-La8JL8pZZqZy02Hrswpkh3I

我还在请求失败并使用一些错误处理中间件后多次捕获到这个数据:

AntiforgeryTokenSet tokens = antiforgery.GetTokens(context);
tokens.CookieToken:  null
tokens.FormFieldName:  "__RequestVerificationToken"
tokens.HeaderName:  "RequestVerificationToken"
tokens.RequestToken:  "CfDJ8F9Fs4CqDFpLttT96eZw9WH33jSw5mM8h7RpEd3vGISQTRkx1rfwm-L2lfkvXKMBc-riESmoTo_fnIjeBbRmOo5KuJHr09f8B75sQ9g_djIVeeaGwMw5KE6W1O2-7Vi03fCnwlTv8l-BWGst76Ln-ZQ"

所以这里有三个字符串:

POST String:  "CfDJ8F9Fs4CqDFpLttT96eZw9WHjWfHO8Yawn35k4Yq3gDK5n1TDJDDiY5o86VQs1_qOVIYBydCizBU4knb7Jmq1-heGhwnMu2KmhUIiAd0xI7Sudv3GX-J0OI6wRfiPL4L1KRs2Pml8dbsDfwemewBqi18"
Cookie String:  "CfDJ8F9Fs4CqDFpLttT96eZw9WFtJht41WcNrmgshi2pFGwcxhr0_0hvINQc7Yl9Cbjhv-TiSNXeEctyKborLI49AcjHfWIgOmmKkbjOe7QMn8Z0WZtkQy5JcaBHKEGTu1p-La8JL8pZZqZy02Hrswpkh3I"
antiforgery.GetTokens(context).RequestToken:  "CfDJ8F9Fs4CqDFpLttT96eZw9WH33jSw5mM8h7RpEd3vGISQTRkx1rfwm-L2lfkvXKMBc-riESmoTo_fnIjeBbRmOo5KuJHr09f8B75sQ9g_djIVeeaGwMw5KE6W1O2-7Vi03fCnwlTv8l-BWGst76Ln-ZQ"

POST字符串和cookie字符串不匹配,但是根据我的经验,即使在ASP.NET Core中请求被认为是合法的情况下,它们也永远不会匹配。但奇怪的是,POST字符串和tokens.RequestToken也不匹配。我认为它们应该匹配,尽管我稍后在请求生命周期中捕获了tokens.RequestToken,所以这可能与此有关。

Github上的ASP.NET Core 2

我决定查看ASP.NET Core 2的源代码。我发现了这个文件,特别是第145行:

https://github.com/aspnet/Antiforgery/blob/dev/src/Microsoft.AspNetCore.Antiforgery/Internal/DefaultAntiforgeryTokenGenerator.cs

这行代码从第134行开始读取文件,获取了消息“防伪cookie令牌和请求令牌不匹配。”

https://github.com/aspnet/Antiforgery/blob/dev/src/Microsoft.AspNetCore.Antiforgery/Resources.resx

所以我认为消息是从那里发起的,但我仍然想知道为什么会发生这种情况。

问题

请有经验的人帮助我解决这些防伪令牌无法验证的问题?用户的Web浏览器是否可能会破坏cookie或POST数据?有没有任何建议?谢谢。


那个cookie的超时时间是多久?也许用户只是等待的时间太长了?人们倾向于将页面保持打开状态数小时。也可能是某个自动程序在最糟糕的时刻清理了数据库中的“有效防伪令牌”。 - Christopher
@user1325179 我看到了非常相似的情况。你最终解决了问题吗? - Gary Brunton
@GaryBrunton,不是。我仍在苦苦挣扎。我几天前将其发布到了ASP.NET Core GitHub问题页面上,但那里还没有给我任何帮助:https://github.com/aspnet/Home/issues/2882。 - user1325179
@user1325179 我们已经找到了问题的根源。我们之前不知道防伪令牌会考虑用户是否已经通过身份验证。在我们的情况下,我们发现一个已经通过身份验证的用户会打开多个包含表单(和防伪令牌)的浏览器标签页。最终他们会从其中一个标签页注销。在注销后,如果他们回到另一个包含表单的标签页并尝试提交它,就会出现错误,因为该令牌是在假定用户已经通过身份验证的情况下生成的。我不确定这是否对您有所帮助。 - Gary Brunton
@GaryBrunton,这并不起作用,因为我没有使用ASP.NET的身份验证系统。不过还是谢谢你。 - user1325179
显示剩余5条评论
4个回答

4

似乎全局禁用过滤器是关闭它的唯一方法。我对@svallis的代码进行了一些修改,使其能够正常工作:

services.AddMvc().AddRazorPagesOptions(options =>
{
    options.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
});

https://github.com/aspnet/Mvc/issues/7012


7
谢谢,但这并不满足我的要求。我想使用防伪系统,而不是禁用它。 - user1325179
@jaybro,我还没有解决它。几个月前我在GitHub上向Microsoft提出了这个问题。他们至今没有给出有意义的回复,这个问题仍然存在。 - user1325179
1
@user1325179 好的,谢谢。我发现我的问题与同一视图上使用令牌的两个表单有关,我通过 asp-antiforgery 属性禁用了其中一个表单,另一个(重要)表单开始工作了。 - jaybro

0
我正在寻找一个解决方案,用于新的.NET 6.0 Blazor Server应用程序,包括身份验证和类似于MSDN的修复,而无需禁用Antiforgery:
 app.UseRouting();

app.UseAuthorization();

var antiforgery = app.Services.GetRequiredService<IAntiforgery>();

app.Use((context, next) =>
{
    var requestPath = context.Request.Path.Value;

    if (string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase)
        || string.Equals(requestPath, "/index.html", StringComparison.OrdinalIgnoreCase))
    {
        var tokenSet = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken!,
            new CookieOptions { HttpOnly = false });
    }

    return next(context);
});

0
关于CookieToken为空的问题:也许是请求被伪造了吗?因为缺少cookie,我怀疑您的网站在每次请求时都会发送它。什么情况下会缺失呢?当它来自其他地方时。
关于另一个问题:
1. 您的中间件,在Configure方法的身份验证部分之后将AntiforgeryToken添加为响应cookie的位置是否正确? 2. 是否可能您在多个GET请求中返回了多个AntiforgeryTokens?在这种情况下,可能存在竞争条件:浏览器使用的最新请求和服务器返回的最新请求可能不同,导致令牌不匹配。
您应该只返回一个AntiforgeryToken,在第一次获取操作时。如果您将前端和后端托管在同一实例上,则可能是根目录(/),如果您在另一个端口上使用API,则应确保在进行POST、PUT或PATCH之前先进行GET操作。
单实例启动示例:
builder.Use(next => context =>
{
    var path = context.Request.Path.Value;
    if (!string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
        context.Request.Method != "GET") return next(context);
    var tokens = antiforgery.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
        new CookieOptions { HttpOnly = false, Path = "/" });

    return next(context);
});

多端口实例(注意CORS)

public IServiceProvider ConfigureServices(IServiceCollection services)
{
  services.AddCors(options =>
  {
     options.AddDefaultPolicy(builder => builder
            .WithOrigins(/*allowed domains here*/)
            .AllowAnyHeader()
            .WithMethods("GET", "POST", "PUT", "DELETE")
            .AllowCredentials()
            .Build()
     );
  });
}
public virtual void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
  app.UseAuthentication()
     .UseAuthorization()
     .Use(next => context =>
      {
         var path = context.Request.Path.Value;
         if (!string.Equals(path, "/api/settings", StringComparison.OrdinalIgnoreCase) ||
             context.Request.Method != "GET") return next(context);
         var tokens = antiforgery.GetAndStoreTokens(context);
         context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
             new CookieOptions { HttpOnly = false });

         return next(context);
      });
}

请记住,如果您将前端托管在另一个端口/位置上,则应像这样添加withCredentials标头:
return this.http.get<Settings>(`/api/settings`, { withCredentials: true });

0

我在这里找到了解决方案:https://github.com/aspnet/Antiforgery/issues/116

using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;


// fix from https://github.com/aspnet/Antiforgery/issues/116
namespace WebAppCore.Code
{
    public class HtmlGeneratorNoStoreAntiforgery: DefaultHtmlGenerator  
    {
        public HtmlGeneratorNoStoreAntiforgery(
            IAntiforgery antiforgery,
            IOptions<MvcViewOptions> optionsAccessor,
            IModelMetadataProvider metadataProvider,
            IUrlHelperFactory urlHelperFactory,
            HtmlEncoder htmlEncoder,
            ValidationHtmlAttributeProvider validationAttributeProvider)
            : base(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder, validationAttributeProvider)
        {
        }

        public override IHtmlContent GenerateAntiforgery(ViewContext viewContext)
        {
            var result = base.GenerateAntiforgery(viewContext);

            viewContext
                    .HttpContext
                    .Response
                    .Headers[HeaderNames.CacheControl]
                = "no-cache, max-age=0, must-revalidate, no-store";

            return result;
        }
    }
}

并在startup.cs中添加:

services.AddSingleton<Microsoft.AspNetCore.Mvc.ViewFeatures.IHtmlGenerator, HtmlGeneratorNoStoreAntiforgery>();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接