在MVC 4应用程序中正确处理HttpAntiForgeryException的方法

38

这是情景:

我有一个登录页面,用户登录后会被重定向到主页应用程序页面。然后用户使用浏览器的返回按钮,现在他回到了登录页面。他尝试再次登录,但现在抛出了一个异常:

HttpAntiForgeryException (0x80004005):提供的防伪标记是给用户“”,但当前用户是“userName”。

我知道这与缓存有关。我使用自定义NoCache过滤器禁用了登录操作的浏览器缓存,并设置了所有必需的头信息- no-cache,no-store,must-revalidate等。但是

  • 这在所有浏览器上都不起作用
  • 特别是Safari(大多数情况下是移动设备)完全忽略此类设置

我将尝试进行hack并强制safari移动设备刷新,但这不是我期望的。

我想知道是否可以:

  • 处理异常而不向用户显示任何问题(对用户完全透明)
  • 通过替换防伪令牌用户名来预防此问题,这将允许用户再次登录而不会出现此异常,如果我的与浏览器缓存相关的hack在下一个版本的浏览器中停止工作。
  • 我真的不想依赖浏览器行为,因为每个浏览器的行为都不同。

更新1

为了澄清一些问题,我知道如何在MVC中处理错误。问题是这种错误处理根本没有解决我的问题。错误处理的基本思想是重定向到带有良好消息的自定义错误页面。但我想要预防这种错误发生,而不是以用户可见的方式处理它。通过处理,我指捕获用户名并替换或采取其他适当的操作,然后继续登录。

更新2

我已添加以下解决方案,对我有用。


我完全赞同您在问题和回答中的澄清更新。 登录逻辑不应依赖空的try-catch、重定向到错误页面、重新加载页面(要求用户立即重新登录 - 在输入他们的凭据后)或在以不同用户身份登录时崩溃(而另一个用户仍然登录)。 - MikeTeeVee
5个回答

22

如果只有一个或少数几个功能受到影响,创建过滤器可能会稍微有些技术上的繁琐。一种更简单但不通用的解决方案是,仅针对特定方法删除 [ValidateAntiForgeryToken],并在检查用户是否已登录后添加手动验证。

if (User.Identity.IsAuthenticated)
{
    return RedirectToAction("Index", "Home");
}
System.Web.Helpers.AntiForgery.Validate();
/* proceed with authentication here */

1
谢谢,这似乎很好地工作,并且仍然提供相同的安全性。 - Garrett Fogerlie
1
这个在我的电脑上不起作用,它给出了完全相同的错误,但是在这一行:System.Web.Helpers.AntiForgery.Validate();。 - Amr Elgarhy
3
@AmrElgarhy,没错,现在你可以理解了! - maxbeaudoin

21

经过一段时间的调查,我认为我找到了一些方法来帮助用户摆脱这个错误。尽管不是完美的,但至少不会显示错误页面:

我基于HandleErrorAttribute创建了一个过滤器:

    [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", 
        Justification = "This attribute is AllowMultiple = true and users might want to override behavior.")]
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public class LoginAntiforgeryHandleErrorAttribute : FilterAttribute, IExceptionFilter
    {
        #region Implemented Interfaces

        #region IExceptionFilter

        /// <summary>
        /// </summary>
        /// <param name="filterContext">
        /// The filter context.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// </exception>
        public virtual void OnException(ExceptionContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (filterContext.IsChildAction)
            {
                return;
            }

            // If custom errors are disabled, we need to let the normal ASP.NET exception handler
            // execute so that the user can see useful debugging information.
            if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
            {
                return;
            }

            Exception exception = filterContext.Exception;

            // If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
            // ignore it.
            if (new HttpException(null, exception).GetHttpCode() != 500)
            {
                return;
            }

            // check if antiforgery
            if (!(exception is HttpAntiForgeryException))
            {
                return;
            }

            filterContext.Result = new RedirectToRouteResult(
                new RouteValueDictionary
                {
                    { "action", "Index" }, 
                    { "controller", "Home" }
                });

            filterContext.ExceptionHandled = true;
        }

        #endregion

        #endregion
    }

然后我将这个过滤器应用到登录POST操作:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[LoginAntiforgeryHandleError]
public ActionResult Login(Login model, string returnUrl)
{
这个解决方案的主要思路是将防伪异常重定向到主索引操作。如果用户仍未通过身份验证,则会显示登录页面,如果用户已通过身份验证,则会显示索引页面。 更新1 这个解决方案存在一个潜在问题。如果有人使用不同的凭据登录,那么应该在错误时添加额外的登录运行时 - 注销先前的用户并登录新用户。此场景未得到处理。

这对我非常有效。我不需要它做任何花里胡哨的事情,只要不抛出黄屏就可以了。 - hal9000

5

通过添加操作过滤器来处理错误,您应该能够处理异常。

[HandleError(View="AntiForgeryExceptionView", ExceptionType = typeof(HttpAntiForgeryException))]

确保在您的web.config中启用了自定义错误功能。
<customErrors mode="On"/>

你还可以查看这篇关于处理错误的 博客 了解更多信息。 编辑:由于你正在使用MVC4,而博客是关于MVC3的,因此你还可以查看MSDN库 - HandleErrorAttribute,但版本实际上并不重要。

是的,但这都是关于重定向的,处理这些错误会中断当前操作(执行)。我想在不中断操作的情况下处理此错误,因此在这种情况下处理、替换用户名并继续使用正确的数据登录。 - Marcin
我认为当你能够捕获错误时,可以将其重定向到一个视图(控制器),在该视图中替换用户名并继续登录,使用先前输入的数据。另请参见:https://dev59.com/m3I-5IYBdhLWcg3wj5FG - Jos Vinke

2

当您之前已经进行过验证时,在登录时会出现此消息。
重现步骤:
1.) 打开登录页面并确认您未经过身份验证。
2.) 复制标签页并在第二个标签页中进行登录。
3.) 返回到第一个标签页并尝试登录(无需重新加载页面)。
4.) 如果您的登录操作带有[ValidateAntiForgeryToken]属性,则会看到此错误:

System.Web.Mvc.HttpAntiForgeryException:
提供的防伪令牌是为用户“”准备的,但当前用户是“YourUserNameOrEmailAddress”。

此助手执行与[ValidateAntiForgeryToken]属性相同的验证:

System.Web.Helpers.AntiForgery.Validate()

从登录操作中删除[ValidateAntiForgeryToken],并使用此方法代替。

现在,当用户已经通过身份验证时,它会重定向到主页。
如果已经通过身份验证,但尝试以其他身份登录,则注销当前用户,并在作为新用户进行身份验证之前继续验证防伪令牌。

if (User.Identity.IsAuthenticated)
{
    if (User.Identity.Name == UserName)//User is already Logged in.
        return RedirectToAction("Index", "Home");
    else//Else: User is Logging In as someone else, so Log Out the Current User.
        ResetUser();
}
System.Web.Helpers.AntiForgery.Validate();//Replaces [ValidateAntiForgeryToken].
//Add your Login Logic below here.

接下来,将此函数添加到安全地重置用户但无需重新加载页面的功能中:

private void ResetUser()
{
    //Add any additional custom Sign-Out/Log-Off Logic here.
    Session.Abandon();
    FormsAuthentication.SignOut();

    //Source: https://dev59.com/um855IYBdhLWcg3w-JbS
    //The User.Identity is Read-Only, but it reads from HttpContext.User, which we may Reset.  Otherwise it will still show as Authenticated until the next Page Load.
    HttpContext.User = new System.Security.Principal.GenericPrincipal(new System.Security.Principal.GenericIdentity(string.Empty), null);//Do not set Identity to null, because other parts of the code may assume it's blank.
}

.net Core 思考:
需要注意的是,如果您正在使用 .net Core 并且在控制器上使用了 [AutoValidateAntiforgeryToken] 属性,或者您已经向整个站点添加了全局过滤器,例如 services.AddMvc(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); });,那么您可以在登录操作方法上使用 [IgnoreAntiforgeryToken] 来避免自动验证异常,并给您机会在继续并手动调用验证辅助方法之前重定向或注销。
注意:我还没有使用 .net Core 进行验证,但为了帮助大家,我在这里分享我的发现。


1

一个老问题 - 但我今天遇到了这个问题,我解决的方法是通过重定向到注销操作:

public ActionResult Login(string returnUrl) 
{
    if (WebSecurity.IsAuthenticated)
        return RedirectToAction("LogOff");

    ...
}

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