MVC5防伪标记 - 如何处理“提供的防伪标记是为用户“”准备的,但当前用户是“xxx”的异常?

24

我希望通过使用AntiforgeryToken属性来保护我们的登录操作-我知道为什么会出现上述异常,但是我似乎找不到任何好的解决方法。

让我们假设有以下情况:

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

  2. 某些用户登录后,意外地按下“返回”按钮并再次尝试登录 - 虽然这更不太可能发生,但它确实会发生,并且当它发生时,我不希望用户看到异常。

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

  1. Global.asax的Application_Start中设置AntiForgeryConfig.SuppressIdentityHeuristicChecks = true; - 它没有解决问题,我仍然会得到相同的异常
  2. 在带有[ValidateAntiForgeryToken]属性的方法上设置[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] - 同样没有成功

现在我想在操作主体中手动验证token,捕获错误,并检查是不是由匿名用户尝试:

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

    //Rest of action body here
    //..
    //..
}
上述方法似乎可以防止错误-但是它是否安全?有什么其他选择吗?
提前感谢。
最好的祝福。
编辑:
最终的“解决方案”是在登录表单上禁用令牌验证——可能有更好的处理方式,但似乎我找到的所有解决方案都像我上面提出的那样丑陋。
由于无法知道这些替代方案有多“安全”(如果它们安全),我们决定在登录时禁用令牌验证。

您可以使用脚本来禁用重复提交,https://dev59.com/gnE85IYBdhLWcg3wVR6g - pool pro
嗨,我在用户点击浏览器的返回按钮时遇到了同样的问题。你最终解决了吗? - VAAA
请查看我的问题的编辑部分 - 由于只有登录表单给我们带来了这个问题,最终我们只是在那里禁用了令牌验证。我们决定这根本不值得麻烦,坦率地说,我找不到任何“干净”的解决方案 - 只有一些丑陋的解决方法(比如我发布的try-catch)。 - user2384366
2个回答

27

请尝试在 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的一个大问题。
但是根据您的应用程序所做的事情,有办法限制他们的强迫症。其中最简单的方法是尽力使访问者不觉得需要“倒带”他们的请求以更改它。
确保表单错误消息清晰简明,以确保用户知道哪里出了问题。上下文错误会给予额外的加分。
始终在表单提交之间保持表单状态。除了密码或信用卡号码外,由于MVC表单助手,没有任何借口。
如果表单分散在选项卡或隐藏的div中,例如在Angular或ember.js等SPA框架中使用的那些表单,请聪明地显示控制器布局或表单,以便在显示错误时显示实际来源的控制器布局或表单。不要只是将它们指向主控制器或第一个选项卡。
“发生了什么?”- 让用户知情
当一个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视图可以告诉他们:“对不起,您尝试提交相同的信息两次”。

另一个想法是记录错误并将用户返回到登录屏幕:

创建一个HandleAntiforgeryTokenErrorAttribute类,覆盖OnException方法。

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) }
        );
    }
}

我建议您使用一些工具来记录所有信息,因为登录是应用程序的重要部分。
通用记录和关键应用程序异常(包括Web异常)的电子邮件使用NLog
使用Elmah来过滤和电子邮件Web异常。
此外,您可能还希望查看名为SafeForm的jQuery插件。链接:Link 对于这个问题,我已经看到了很多争论,每个人的观点都有其合理的观点。我的看法是(来自owasp.org):
跨站请求伪造(CSRF)是一种攻击,它迫使最终用户在他们“当前已验证”的Web应用程序上执行不需要的操作,CSRF攻击特别针对改变状态的请求,而不是数据窃取。防伪令牌是特定于“谁正在登录”的。因此,一旦您登录,然后返回,旧令牌将不再有效。
现在我也使用授权IP地址登录到我的许多应用程序,并在用户IP地址更改时使用2因素授权,因此,如果存在跨站请求伪造,用户将不匹配IP地址并请求2因素授权。几乎就像安全路由器的工作方式。但是,如果您想在登录页面上保持它,只要设置友好的错误页面,人们就不会生气,因为他们会看到他们做错了什么。

感谢您的回答,很抱歉回复晚了 - 我稍微修改了您的解决方案(我使用了隐藏输入字段来传递:"已提交"的值),现在“返回按钮”情况似乎已经得到解决,但是AD1中仍然存在情况,这可以通过在浏览器中加载两个不同选项卡中的登录表单来模拟,我能清楚地看到 "___RequestverificationToken" cookie 在两个选项卡中都是相同的,在提交时我会收到与之前相同的错误。虽然我可以捕获此异常,但我仍然不知道该怎么处理... - user2384366
真相是,对于这个问题的真正答案就是:你不应该在登录表单上使用防伪令牌!在登录表单上“仿冒”用户是没有意义的——在用户登录之前,他们还没有登录,都是匿名用户!因此,应该使用匿名用户来创建请求验证令牌,并且防伪令牌应该在用户登录后的表单中使用,因为同一浏览器和电脑上的标签页之间没有任何差别。 - pool pro
1
说实话 - 我现在有点困惑,因为对于这个问题似乎存在两种截然不同的观点,例如在这里:http://security.stackexchange.com/questions/2120/when-the-use-of-a-antiforgerytoken-is-not-required-needed我找到了一个非常详细的回答和示例,它指出在登录表单上应该使用防伪令牌... 另一方面,还有很多其他帖子声称相反...我会再等一段时间,看看是否有其他可能的想法 - 但如果没有其他意见,我想我会按照您建议的那样,在登录时禁用令牌验证 :) - user2384366
你尝试过在登录控制器中使用 '[OutputCache(NoStore = true, Location = OutputCacheLocation.None)]' 吗?这会自动为页面加载创建一个新的请求。 - pool pro
非常抱歉回复晚了。我尝试过使用“[OutputCache(NoStore = true,Location = OutputCacheLocation.None)]”,但它并不能防止任何情况的发生(可能是因为浏览器在用户点击“返回”时仍然使用自己的缓存)。无论如何,感谢所有的建议 - 目前,我们将在登录表单上禁用防伪令牌 - 它似乎比它的价值更麻烦。 - user2384366
显示剩余3条评论

1
我只想补充一下pool pro's answer - 关于筛选器部分 - 必须小心,因为那将覆盖所有关于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] 的方法区分开来。

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