19

我们有一个在 Windows Azure(最新的 2.x OS 版本)上运行的 ASP.NET MVC 2 (.NET 4) 应用程序,带有两个 Web 角色实例。

我们使用 MVC 为所有 POST 请求提供的防伪令牌,并且我们在 web.config 中设置了一个静态机器密钥,因此一切都可以在多台机器上运行并跨重启。99.9% 的情况下它都能完美运行。

然而,我们时不时地记录一个 HttpAntiForgeryException,并显示消息“未提供所需的防伪令牌或无效”。

我知道问题可能是浏览器中不允许使用 cookie,但我们已经验证了 cookie 已启用并且可以正确地来回发送。

该错误发生在各种浏览器上,显然会给用户带来问题,因为他们必须重复操作,否则可能会丢失一些数据。可以说,我们无法在本地重现该问题,但它只发生在 Windows Azure 上。

为什么会这样?我们怎样才能避免呢?

4

5 回答 5

20

我最近也遇到了这个问题,发现了两个原因。

1. 浏览器在打开页面时恢复缓存的最后一个会话

如果您有一个可缓存的页面,该页面执行到您的服务器的发布(即防伪将打开)并且用户将其浏览器设置为在启动时恢复最后一个会话(此选项存在于 chrome 中)该页面将从缓存中呈现. 但是,请求验证 cookie 将不存在,因为它是浏览器会话 cookie,并在浏览器关闭时被丢弃。由于 cookie 不见了,您将获得防伪异常。解决方案:返回响应头,使页面不被缓存(即 Cache-Control:private, no-store)。

2. 如果在您的网站启动时打开多个选项卡,则会出现竞争条件

浏览器可以选择在启动时打开一组选项卡。如果其中不止一个访问您的站点并返回请求验证 cookie,您可能会遇到请求验证 cookie 被覆盖的竞争条件。发生这种情况是因为一个没有设置请求验证 cookie 的用户向您的服务器发送了多个请求。处理第一个请求并设置请求验证 cookie。接下来处理第二个请求,但它没有发送 cookie(在请求时尚未设置),因此服务器生成一个新的。新的会覆盖第一个,现在该页面将在下次执行发布时获得防伪请求异常。MVC 框架不处理这种情况。此错误已报告给 Microsoft 的 MVC 团队。

于 2012-05-11T16:31:10.800 回答
8

防伪令牌发出时包含当前连接用户的用户名。并且在验证其有效性时,会根据发出令牌时使用的用户检查当前连接的用户。因此,例如,如果您有一个用户尚未经过身份验证的表单,并且您发出了一个防伪令牌,则不会有任何用户名存储在其中。如果在提交表单时对用户进行身份验证,则令牌将不再有效。同样适用于注销。

下面是 Validate 方法的样子:

public void Validate(HttpContextBase context, string salt)
{
    string antiForgeryTokenName = AntiForgeryData.GetAntiForgeryTokenName(null);
    string str2 = AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath);
    HttpCookie cookie = context.Request.Cookies[str2];
    if ((cookie == null) || string.IsNullOrEmpty(cookie.Value))
    {
        throw CreateValidationException();
    }
    AntiForgeryData data = this.Serializer.Deserialize(cookie.Value);
    string str3 = context.Request.Form[antiForgeryTokenName];
    if (string.IsNullOrEmpty(str3))
    {
        throw CreateValidationException();
    }
    AntiForgeryData data2 = this.Serializer.Deserialize(str3);
    if (!string.Equals(data.Value, data2.Value, StringComparison.Ordinal))
    {
        throw CreateValidationException();
    }
    string username = AntiForgeryData.GetUsername(context.User);
    if (!string.Equals(data2.Username, username, StringComparison.OrdinalIgnoreCase))
    {
        throw CreateValidationException();
    }
    if (!string.Equals(salt ?? string.Empty, data2.Salt, StringComparison.Ordinal))
    {
        throw CreateValidationException();
    }
}

一种可能的调试方法是从其源代码重新编译 ASP.NET MVC,并准确记录您在引发异常时输入的哪些 if 情况。

于 2012-03-29T15:08:49.667 回答
1

我有一些 MVC3 网络应用程序也经常得到这个。其中大多数是因为客户端没有发送 POST 正文。其中大部分是 IE8,因为在常规表单发布之前存在 ajax 请求的一些错误。有一个针对 IE 的修补程序似乎可以解决这些症状,这可以证明在这些情况下它是一个客户端错误

http://support.microsoft.com/?kbid=831167

网上有一些关于这个问题的讨论,但没什么有用的,我绝对不会搞乱保持活动超时,这在某些地方是建议的“解决方案”......

https://www.google.com/search?q=ie8+empty+post+body

我一直无法通过各种尝试重置 POST 之间的连接来重现它,所以对于 IE 空 POST 正文的情况,恐怕我没有真正的解决方案。我们减轻它的方法是确保我们在通过 ajax 检索数据时从不使用 POST 方法。

如果您记录完整的请求,请检查 POST 正文是否为空,如果是,则可能是较旧的 IE。而且我的意思不是 Content-Length: 0,它通常会有一个 Content-Length 在标头中看起来是正确的,但在请求中的标头之后实际上什么都没有。

整个问题对我来说仍然是个谜,因为我们仍然偶尔会遇到有完整 POST 正文的异常。我们的用户名永远不会改变,我们的密钥也是静态的,我还没有尝试向源代码添加调试,如果我能解决这个问题,我会报告我的发现。

于 2012-09-25T18:48:04.997 回答
0

我自己自制的防伪代码也遇到过类似的问题,它在概念上与 MVC 机制非常相似。大多数问题似乎是因为现代浏览器似乎愿意显示指定为非缓存页面的缓存副本。

我已经尝试了页面无缓存指令的所有组合,但有时我仍然会显示缓存页面。

我发现更好的解决方案是为页面挂钩 onbeforeunload 事件,并明确清除 DOM 中保存令牌值的隐藏输入字段的值。

如果加载了页面的缓存副本,它似乎包含已清除的输入字段值。然后我在文档准备功能中对此进行测试,并在必要时重新加载页面:

window.location.reload(true);

似乎工作得非常有效,我怀疑它也可能适用于 MVC 防伪代码。

于 2013-05-03T11:19:29.287 回答
0

您可以尝试几种选择。您可以尝试远程连接到机器并查看事件日志,看看是否可以从中获得更多关于发生这种情况的信息。如果这没有帮助,您可以使用 DebugDiag 或其他一些工具来捕获进程的转储(DebugDiag 将让您在发生此特定异常时捕获一个)。然后看看它,看看发生了什么。

如果您似乎无法从那里弄清楚,您可以随时向 Microsoft 创建一个支持案例来帮助您进行调查。

于 2012-03-29T14:36:08.397 回答