MVC5 AntiForgeryToken - 如何处理 "The provided anti-forgery token was meant for user "",但当前用户是 "xxx"。"例外?

MVC5 AntiForgeryToken - how to handle "The provided anti-forgery token was meant for user "", but the current user is "xxx"." exception?

我想通过 AntiforgeryToken 属性来保护我们的登录操作 - 我知道为什么会出现主题异常,但是我似乎找不到任何好的解决方案。

假设我们有以下情况:

  1. 现在是 8:00 上午,应用程序用户正在上班,他们坐下来开始登录过程 - 现在 很有可能某些用户将获得相同的 ValidationToken。第一个登录后 - 所有其他人在尝试登录时都会看到上述异常(或其他一些自定义异常屏幕)。

  2. 一些用户登录后,不小心按下了“后退”按钮并尝试再次登录 - 虽然这种情况不太可能发生,但还是有可能发生,而且我不希望用户看到异常。

所以问题很简单——如何防止上述情况,或者如何处理它们,使用户不会注意到任何事情。我尝试了以下方法:

  1. 设置 AntiForgeryConfig.SuppressIdentityHeuristicChecks = true; in Application_Start in Global.asax - 它没有解决问题,我仍然得到同样的异常
  2. 在具有 [ValidateAntiForgeryToken] 属性的方法上设置 [OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] - 再次,运气不好

现在我正在考虑手动验证操作正文中的令牌,捕获错误,并检查是否由匿名用户进行尝试:

public ActionResult SomeAction()
{
    try
    {
        AntiForgery.Validate();
    }
    catch(HttpAntiForgeryException ex)
    {
        if(String.IsNullOrEmpty(HttpContext.User.Identity.Name))
        {
            throw;
        }
    }

    //Rest of action body here
    //..
    //..
}

以上似乎可以防止错误 - 但是它安全吗? 还有哪些替代方案?

提前致谢。

此致。

编辑:

最后的 "solution" 是在登录表单上禁用令牌验证 - 可能有更好的方法来处理它,但似乎我找到的所有解决方案都是与我上面提出的类似的丑陋解决方法。

由于无法知道 "safe" 这些替代方案如何(如果它们完全安全),我们决定在登录时禁用令牌验证。

尝试设置(在global.cs中):

AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;

这会将名称标识符添加到您的令牌中,

关于双重登录问题,尝试使用脚本来记录原始提交的日期和时间,以阻止使用相同令牌的第二次提交。

// jQuery plugin to prevent double submission of forms
jQuery.fn.preventDoubleSubmission = function() {
  $(this).on('submit',function(e){
    var $form = $(this);

    if ($form.data('submitted') === true) {
      // Previously submitted - don't submit again
      e.preventDefault();
    } else {
      // Mark it so that the next submit can be ignored
      $form.data('submitted', true);
    }
  });

  // Keep chainability
  return this;
};

所以我们知道一件事;用户喜欢后退按钮并且有双击的习惯,这是 AntiforgeryToken 的一个大问题。

但是根据您的应用程序的用途,有一些方法可以限制他们这样做的冲动。最简单的方法是尽最大努力让访问者觉得他们不需要“倒回”他们的请求来改变它。

Ensure that form error messaging is clear and concise to ensure the user knows what is wrong. Contexual errors give bonus points.

Always maintain form state between form submissions. Apart from passwords or credit card numbers, there’s no excuse thanks the to MVC form helpers. @Html.LabelFor(x => x.FirstName)

If forms are spread across tabs or hidden divs such as those used in SPA frameworks like Angular or ember.js, be smart and show the controller layouts or form that the errors actually originated from in the form submission when displaying the error. Don’t just direct them to the home controller or first tab.

“怎么回事?” - 让用户了解情况

当 AntiForgeryToken 未验证您的网站时,将抛出类型为 System.Web.Mvc.HttpAntiForgeryException 的异常。

如果您设置正确,就会打开友好错误,这意味着您的错误页面不会显示异常,而是显示一个漂亮的错误页面,告诉他们发生了什么。

您至少可以通过捕获 HttpAntiForgeryException 为用户提供针对这些异常的信息更丰富的页面,从而使这更容易一些。

private void Application_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError();

    if (ex is HttpAntiForgeryException)
    {
        Response.Clear();
        Server.ClearError(); //make sure you log the exception first
        Response.Redirect("/error/antiforgery", true);
    }
}

而您的 /error/antiforgery 视图可以告诉他们 抱歉,您已尝试两次提交相同的信息

另一个想法是记录错误,return 用户到登录屏幕:

创建一个覆盖 OnException 方法的 HandleAntiforgeryTokenErrorAttribute class。

HandleAntiforgeryTokenErrorAttribute.cs:

public class HandleAntiforgeryTokenErrorAttribute : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        filterContext.ExceptionHandled = true;
        filterContext.Result = new RedirectToRouteResult(
            new RouteValueDictionary(new { action = "Login", controller = "Account" }));
    }
}

全局过滤器:

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new HandleAntiforgeryTokenErrorAttribute()
            { ExceptionType = typeof(HttpAntiForgeryException) }
        );
    }
}

我还会使用一些工具来记录您的所有信息,因为登录是您应用程序的关键部分

NLog 用于有关关键应用程序异常(包括 Web 异常)的一般日志记录和电子邮件。

Elmah 用于 Web 异常的过滤和电子邮件。

编辑: 此外,您可能还想看看名为 SafeForm 的 jQuery 插件。 Link

编辑:

我看到了很多关于这个问题的争论,每个人对这个问题的看法都有道理,我是怎么看的(摘自owasp.org

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated, CSRF attacks specifically target state-changing requests, not theft of data. The anti-forgery token is specific to 'who is logged on'. So once you login, then go back, the old token is no longer valid

现在我也使用授权的 IP 地址登录,如果用户 IP 地址发生变化,我的应用程序将获得 2 因素授权,因此如果跨站点请求伪造在起作用,用户将不会匹配 IP 地址,并且请求 2 因子授权。几乎就像安全路由器的工作方式一样。但是如果你想把它保留在你的登录页面上,我看不出有什么问题,只要你设置了友好的错误页面,人们就不会生气,因为他们会看到他们做错了什么。

我只是想添加到 - 关于过滤器部分 - 必须小心,因为这将 覆盖所有异常 关于 AntiforgeryToken,如果您(像我一样)在应用程序的其他部分进行了此令牌验证,您可能会考虑向过滤器添加一些验证:

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new HandleAntiforgeryTokenErrorAttribute() { ExceptionType = typeof(HttpAntiForgeryException) });
    }
}

public class HandleAntiforgeryTokenErrorAttribute : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        string actionName = filterContext.Controller.ControllerContext.RouteData.Values["action"].ToString();
        string controllerName = filterContext.Controller.ControllerContext.RouteData.Values["controller"].ToString();

        if (actionName.ToLower() == "login" && controllerName.ToLower() == "account")
        {
            //Handle Error
            //In here you handle the error, either by logging, adding notifications, etc...

            //Handle Exception
            filterContext.ExceptionHandled = true;
            filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary(new { action = "Login", controller = "Account" }));
        }
        else
        {
            base.OnException(filterContext);
        }
    }
}

请注意,我将 Account/Login 方法抛出此异常的情况与可能使用 [ValidateAntiForgeryToken] 的所有其他方法分开。