解决防伪令牌问题的疑难杂症

36

我有一个表单提交,但总是提示我反伪造令牌错误。

这是我的表单:

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.EditorFor(m => m.Email)
    @Html.EditorFor(m => m.Birthday)
    <p>
        <input type="submit" id="Go" value="Go" />
    </p>
}

这是我的操作方法:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Join(JoinViewModel model)
{
    //a bunch of stuff here but it doesn't matter because it's not making it here
}

这是在web.config中的machineKey:

<system.web>
  <machineKey validationKey="mykey" decryptionKey="myotherkey" validation="SHA1" decryption="AES" />
</system.web>

这是我收到的错误信息:

A required anti-forgery token was not supplied or was invalid.

我读到过在HttpContext上更改用户将会使令牌失效,但这里并没有发生。我的Join操作的HttpGet只是返回视图:

[HttpGet]
public ActionResult Join()
{
    return this.View();
}

所以我不确定发生了什么。我搜索了一下,似乎所有的东西都表明这要么是机器密钥更改(应用程序周期),要么是用户/会话更改。

还有什么其他情况可能发生?我该如何进行故障排除?


7
这个页面在18个月内被浏览了将近4000次,但没有其他人发现要复制它,你所需要做的就是双击登录按钮? - Simon_Weaver
可能是重复提交表单的问题 防止jQuery中的表单重复提交 - Bohdan Lyzanets
3
双重提交是触发反伪造令牌异常的一种方式。从下面的代码中可以看出,有许多不同的情况可能会引发此异常,在我的情况下,与双重提交无关。 - Jerad Rose
11个回答

34
我不知道你的意思是说你能按需获取错误,还是在日志中看到它,但无论如何,这里有一种方法可以保证产生一个防伪令牌错误。
等待...
- 确保已注销,然后输入登录信息。 - 双击登录按钮。 - 会出现以下内容:
提供的防伪令牌是给用户“”,但当前用户是“XXX@yahoo.com”。
(暂时假设这个确切的错误消息在 MVC4 中发生了变化,并且这基本上是您正在收到的相同的消息。)
有很多人还是双击所有东西 - 这是不好的!我刚刚醒来就弄清楚了这个问题,所以我真的不知道这是如何通过测试的。您甚至不需要双击 - 如果按钮无响应,我第二次单击时也会出现此错误。
我只是删除了验证属性。我的网站始终是 SSL,我对风险并不过于担心。我现在只需要它能够正常工作。另一种解决方案是使用 JavaScript 禁用按钮。
这可以在 MVC4 初始安装模板上复制。

如果有人发现与此问题相关的连接问题,请在评论中发布。 - Simon_Weaver
3
我用以下代码来防止这种问题: $('#loginForm form').submit(function () { $(this).find('input[type=submit]').attr("disabled","disabled"); }); 该代码的作用是在提交表单时禁用提交按钮,从而防止重复提交等问题。 - Bohdan Lyzanets
@Bohdan,你应该把你的评论变成一个答案。我知道是什么导致了我的问题(用户双击),但是我正在寻找一个快速解决方案。你的JS非常有效。 - Will D
@Simon_Weaver 我正在使用MVC5,双击仍然会引发这个错误。 - Will D
我“希望”这就是我遇到的问题。有点难过的是,它在MVC5中仍然存在问题... - ganders
显示剩余2条评论

19

在得到Adam的帮助后,我将MVC源代码添加到了我的项目中,并且发现有许多情况会导致相同的错误。

这里是用于验证防伪令牌的方法:

    public void Validate(HttpContextBase context, string salt) {
        Debug.Assert(context != null);

        string fieldName = AntiForgeryData.GetAntiForgeryTokenName(null);
        string cookieName = AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath);

        HttpCookie cookie = context.Request.Cookies[cookieName];
        if (cookie == null || String.IsNullOrEmpty(cookie.Value)) {
            // error: cookie token is missing
            throw CreateValidationException();
        }
        AntiForgeryData cookieToken = Serializer.Deserialize(cookie.Value);

        string formValue = context.Request.Form[fieldName];
        if (String.IsNullOrEmpty(formValue)) {
            // error: form token is missing
            throw CreateValidationException();
        }
        AntiForgeryData formToken = Serializer.Deserialize(formValue);

        if (!String.Equals(cookieToken.Value, formToken.Value, StringComparison.Ordinal)) {
            // error: form token does not match cookie token
            throw CreateValidationException();
        }

        string currentUsername = AntiForgeryData.GetUsername(context.User);
        if (!String.Equals(formToken.Username, currentUsername, StringComparison.OrdinalIgnoreCase)) {
            // error: form token is not valid for this user
            // (don't care about cookie token)
            throw CreateValidationException();
        }

        if (!String.Equals(salt ?? String.Empty, formToken.Salt, StringComparison.Ordinal)) {
            // error: custom validation failed
            throw CreateValidationException();
        }
    }

我的问题是,它将Identity用户名与表单令牌的用户名进行比较。在我的情况下,我没有设置用户名(一个为null,另一个为空字符串)。

虽然我怀疑很少有人会遇到这种情况,但希望其他人能够发现正在被检查的基本条件。


顺便问一下,你是怎么遇到空字符串和空用户名的情况的? - Adam Tuliper
有点奇怪的情况,这就是为什么很难进行故障排除的原因。但我将我的自定义身份设置为使用用户电子邮件作为用户名。后来,我使其在注册时不需要电子邮件(Twitter API不提供电子邮件),因此在这些情况下,电子邮件为空。我认为表单用户名为空,并将其与空标识.UserName进行比较,结果失败了。我忘记了Identity依赖于电子邮件,所以现在我只是让它使用用户ID。再次感谢您的帮助。 - Jerad Rose
我遇到了完全相同的问题...使用Windows身份基础...并在博客中写了关于它的内容。非常感谢你提示我开始寻找的地方...我会说这是MVC中的一个错误(尽管很微妙...)http://erikbra.wordpress.com/2011/08/10/mvc-antiforgerytoken-and-wif-gotcha/ - Erik A. Brandstadmoen
1
这个验证函数,特别是抛出的异常,在MVC5中是否有更新,以便提供更具描述性的错误信息,帮助管理员知道根本原因来自哪里?在我看来,对于5种不同的情况使用通用响应并不是好的架构/编码。 - ganders
哪个类包含这个方法? - jpaugh

9

AntiForgeryToken 还会检查您已登录的用户凭证是否发生变化 - 这些凭证也被加密在 cookie 中。您可以通过在 global.asax.cs 文件中设置 AntiForgeryConfig.SuppressIdentityHeuristicChecks = true 来关闭此功能。


6
对我来说不是一个解决方案...我尝试过了,但它并没有起作用。 - Ibrahim Amer
1
设置这个的安全影响是什么? - johnstaveley

8
您应该防止重复提交表单。 我使用以下代码来防止此类问题:
$('#loginForm').on('submit',function(e){
    var $form = $(this);

    if (!$form.data('submitted') && $form.valid()) {
      // mark it so that the next submit can be ignored
      $form.data('submitted', true);
      return;
    }

    // form is invalid or previously submitted - skip submit
    e.preventDefault();
});

或者
$('#loginForm').submit(function () {
    $(this).find(':submit').attr('disabled', 'disabled'); 
});

这是不好的:如果您使用客户端JavaScript表单验证,即使表单无效,您的代码也会禁用提交按钮,这意味着用户无法修复客户端错误并重新提交。 - Will Appleby
您可以使用jQuery的.valid()方法,在禁用提交按钮之前检查表单是否有效。 - Will Appleby
在我写完JS版本后看到了这个答案。这应该更好:$("form").on("submit", function (e) { var self = $(this); if (!self[0].checkValidity() || self.data("submitted")) { e.preventDefault(); return; } self.data("submitted", true); }); - Gup3rSuR4c
这个答案对我来说似乎是有效的,直到我意识到在客户端验证中,即使字段存在错误,表单也被认为已提交,实际上没有真正提交任何内容。 - Word Rearranger

3

您是在单个服务器上运行还是在Web Farm上运行?如果是单个服务器,请在web.config文件中注释掉machineKey元素,并以此作为基本起点再次尝试。有任何改变吗? 另外,您能否想到任何原因导致您的cookie被清除或过期 - 它们对于此功能的正常工作也是必需的。


我在Mac上通过Windows虚拟机运行这个程序。但是我一直待在虚拟机里。服务器是由Visual Studio(2010)启动的开发服务器。我还使用主机头记录将“真实”URL路由到我的本地计算机(即使得生产环境的URL指向我的本地计算机)。关于machineKey,起初我没有添加它,后来出现了问题才加上的。所以无论如何都有问题。 - Jerad Rose
检查cookie是否有效并且已经发送到正确的域名服务器。使用Fiddler检查cookie域名是否正确。您是否一直在此主机地址内(而不是在任何ajax函数中切换回localhost而不是yoursite.com)。令牌不依赖于会话,因此会话结束不应该有影响。您可以获取MVC源代码并进入方法以验证其失败的位置。 - Adam Tuliper
谢谢Adam,我会这样做。我要说的是,这涉及到与Twitter的一些交互。基本上,我有(我)TwitterAuthorize ->(twitter)Twitter的授权->(我)TwitterCallback ->(我)Join。但调用此页面的页面位于我的服务器上,因此我认为这个远程Twitter调用不是一个因素。 - Jerad Rose
更新 - 现在看来问题似乎只出现在我的第二个测试账户上。由于某种原因,我的第一个测试账户可以正常发布表单,但是我的第二个账户始终无法通过防伪令牌验证。检查cookies,两个账户的cookies看起来几乎相同,数量相同,名称相同,域名相同。当然,值不同。 - Jerad Rose
1
Doh - 对我来说,下一步将是MVC调试源代码以查看失败原因、值等。这段代码相当简单-请查看此帖子以获取设置说明:http://weblogs.asp.net/gunnarpeipman/archive/2010/07/04/stepping-into-asp-net-mvc-source-code-with-visual-studio-debugger.aspx - Adam Tuliper
显示剩余7条评论

3

我刚遇到一个问题,其中@Html.AntiForgeryToken()被调用了两次,因此抗伪标记在HTTP POST有效负载中出现问题。


2

在禁用提交按钮之前,必须检查表单是否有效。

<script type="text/javascript">
//prevent double form submission
$('form', '#loginForm').submit(function () {
    if ($(this).valid()) {
        $(this).find(':submit').attr('disabled', 'disabled');
    }
});


1
我在将我的应用迁移到新机器时也遇到了这个问题。我不明白为什么突然出现了这个错误,但确信它与迁移有关,因此开始调查通过 aspnet_reqsql 注册数据库时的情况。当这一点被排除后,我意识到必须与应用程序的注册有关,并在类似的地方找到了答案。在我的旅程中,我还发现了解决这个问题的两种方法。
  1. ASP.NET会为每个应用程序自动生成一个加密密钥,并将该密钥存储在HKCU注册表中。当应用程序在服务器集群上迁移或访问时,这些密钥不匹配,因此第一种方法是向web.config添加唯一的机器密钥。可以从IIS管理控制台生成机器密钥,并将其添加到web.config的<machineKey>部分。

    <machineKey validationKey="DEBE0EEF2A779A4CAAC54EA51B9ACCDED506DA2A4BEBA88FA23AD8E7399C4A8B93A006ACC1D7FEAEE56A5571B5AB6D74819CFADB522FEEB101B4D0F97F4E91" decryptionKey="7B1EF067E9C561EC2F4695578295EDD5EC454F0F61DBFDDADC2900B01A53D4" validation="SHA1" decryption="AES" />

  2. 第二种方法是通过AspNet_RegIIS和开关-ga为工作进程授予对HKCU注册表的访问权限,或者使用-i为所有应用程序授予访问权限。

aspnet_regiis -ga "IIS APPPOOL\app-pool-name"

无论你选择哪种方法都应该解决这个问题,但我认为最可靠的方式是在web.config中使用唯一键,这样可以保证未来迁移和服务器更改的稳定性,需要注意的是,这将导致应用程序覆盖HKCU注册表项并使其继续运行。

1

还有一件可能导致这个错误的事情需要检查:我在一个表单中使用了两个@Html.AntiForgeryToken()

一旦删除其中一个,问题就会消失。


1
我刚遇到了类似的问题。我得到了:
The required anti-forgery form field "__RequestVerificationToken" is not present. 

有趣的是,我尝试调试它并在控制器中找到了我期望的两个位置都看到了令牌。
var formField = HttpContext.Request.Params["__RequestVerificationToken"];
var cookie = System.Web.HttpContext.Current.Request.Cookies["__RequestVerificationToken"].Value;

实际上,我应该在这里寻找:

var formField = HttpContext.Request.Form["__RequestVerificationToken"];

作为Params包含的不仅仅是表单字段,还包括查询字符串、表单、Cookie和ServerVariables。
一旦那个误导性的线索被排除掉,我发现AntiForgeryToken在错误的表单中!

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