防伪令牌是为用户“”设计的,但当前用户是“用户名”。

138

我正在构建一个单页应用程序,并遇到了反跨站点请求伪造令牌的问题。

我知道问题出在哪里,但不知道如何解决它。

当发生以下情况时,我会收到错误:

  1. 未登录的用户加载带有生成的反跨站点请求伪造令牌的对话框
  2. 用户关闭对话框
  3. 用户登录
  4. 用户打开相同的对话框
  5. 用户在对话框中提交表单

反跨站点请求伪造令牌是为用户 "" 设计的,但当前用户是 "username"

造成这种情况的原因是我的应用程序是100%的单页应用程序,当用户通过ajax post成功登录到/Account/JsonLogin时,我只需使用从服务器返回的“已验证视图”替换当前视图,但不重新加载页面。

我知道这是原因,因为如果我在第3步和第4步之间简单地重新加载页面,则不会出现错误。

因此,似乎加载的表单中的@Html.AntiForgeryToken()仍然为旧用户返回令牌,直到重新加载页面。

我该如何更改@Html.AntiForgeryToken()以返回新的已验证用户令牌?

我在每个Application_AuthenticateRequest上注入了一个自定义的IIdentity,带有一个自定义的GenericalPrincipal,因此当调用@Html.AntiForgeryToken()时,HttpContext.Current.User.Identity实际上是我的自定义标识,其IsAuthenticated属性设置为true,但似乎@Html.AntiForgeryToken仍然会呈现旧用户的令牌,除非我重新加载页面。


你能否在不重新加载的情况下验证 @Html.AntiForgeryToken 代码是否被调用了? - Kyle C
当然可以,我可以顺利进入那里并检查HttpContext.Current.User对象,就像我之前提到的那样。 - parliament
2
请参考此链接:https://dev59.com/eWQn5IYBdhLWcg3w_LTx#19471680 - Rosdi Kasim
@parliament,您能告诉我在下面的答案中您选择了哪个选项吗? - Siddharth Pandey
我记得我曾经做了一个例外,选择进行完全重新加载。但是我预计在新项目中很快会遇到这个问题。如果我选择更好的工作选项,我会回复的。 - parliament
这个回答解决了您的问题吗?在MVC 4应用程序中正确处理HttpAntiForgeryException的方法 - MikeTeeVee
10个回答

174
这是因为反伪造标记将用户的用户名作为加密令牌的一部分嵌入其中以进行更好的验证。当您首次调用@Html.AntiForgeryToken()时,用户尚未登录,因此令牌的用户名部分将为空字符串。在用户登录之后,如果您不替换反伪造令牌,则它将无法通过验证,因为初始令牌是针对匿名用户的,现在我们有了已知用户名的经过身份验证的用户。
您有几个选项来解决这个问题:
  1. 仅在这次让您的SPA执行完整的POST操作,当页面重新加载时,它将具有更新的用户名嵌入的反伪造令牌。

  2. 使用仅包含@Html.AntiForgeryToken()的部分视图,在登录后立即执行另一个AJAX请求,并使用响应替换现有的反伪造令牌。

请注意,设置AntiForgeryConfig.SuppressIdentityHeuristicChecks = true并不会禁用用户名验证,它只是改变了该验证的工作方式。请参见ASP.NET MVC文档,读取该属性的源代码,以及验证令牌中的用户名的源代码,无论该配置项的值如何。

21
@parliament:您接受了这个答案,能否与我们分享一下您选择了哪个选项? - R. Schreurs
9
支持简单易懂的选项3,OAuth提供商设置的定时登出也会导致此问题。 - iCollect.it Ltd
18
选项3对我没用。在注销状态下,我打开了两个登录页面窗口,在一个窗口中作为一个用户登录,在另一个窗口中以另一个用户身份登录,但收到了相同的错误提示。 - McGaz
6
很遗憾,对于这个问题我无法找到一个好的解决方案。我已经在登录页面上删除了该标记。但在登录后的帖子中仍然包含该标记。 - McGaz
8
选项3对我也没有用。仍然出现相同的错误。 - Joao Leme
显示剩余13条评论

26
为了修复错误,您需要在登录页面的 Get ActionResult 上放置 OutputCache 数据注释,如下所示:
[OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")] 
public ActionResult Login(string returnUrl)

我的使用情况是用户尝试登录,通过ModelState.AddError()显示错误,例如“帐户已禁用”。然后,如果他们再次点击登录,他们将看到此错误。然而,这个修复只是给了他们一个空的新登录视图,而不是防伪令牌错误。因此,这不是一个解决方案。 - yourpublicdisplayname
我的案例:
  1. 用户登录() 并进入主页。
  2. 用户点击返回按钮,返回到登录视图。
  3. 用户再次登录,看到错误提示“Anti forgery token is meant for user "" but the current user is ""username""”。在错误页面上,如果用户从菜单中点击任何其他选项卡,则应用程序将按预期工作。
使用上述代码,用户仍然可以点击后退按钮,但会被重定向到主页。因此,无论用户点击多少次后退按钮,它都将重定向到主页。谢谢。
- Jas
为什么这个在 Xamarin 的 Webview 上不起作用? - Noobie3001
1
完整的解释请参见 使用输出缓存提高性能 - stomy

17

当您已经通过身份验证并登录时,将出现此消息。

此辅助程序与[ValidateAntiForgeryToken]属性完全相同。

System.Web.Helpers.AntiForgery.Validate()

从控制器中删除[ValidateAntiForgeryToken]属性,将此帮助程序放置在操作方法中。

因此,当用户已经经过身份验证时,请重定向到主页;如果未经身份验证,则继续验证有效的防伪令牌。

if (User.Identity.IsAuthenticated)
{
    return RedirectToAction("Index", "Home");
}

System.Web.Helpers.AntiForgery.Validate();
为了尝试复现错误,请按照以下步骤操作:如果您在登录页面上并且未经过身份验证,请复制选项卡,在第二个选项卡中登录。然后,如果您回到登录页面的第一个选项卡并尝试在不重新加载页面的情况下登录...则会出现此错误。

1
非常棒的解决方案!在尝试了许多其他建议都没有起作用之后,这个解决了我的问题。首先,重现错误很麻烦,直到我发现可能是因为同时打开了两个浏览器或选项卡,并且用户从一个选项卡登录,然后从第二个选项卡登录而没有重新加载页面。 - Nicki
谢谢这个解决方案。对我也有用。我添加了一个检查,以查看身份是否与登录用户名相同,如果是,我会继续尝试登录用户,并在不成功时将其注销。例如, 尝试 { System.Web.Helpers.AntiForgery.Validate();} catch (HttpAntiForgeryException) { if (!User.Identity.IsAuthenticated || string.Compare(User.Identity.Name, model.Username) != 0) { // 在此处编写您的注销逻辑 } } - Steve Owen
希望你不介意,我使用了这个答案,并结合了我发现的其他逻辑(用于处理以不同身份登录)来回答一个旧问题:https://dev59.com/-mcs5IYBdhLWcg3wSiFo#68685999。感谢你的帮助! - MikeTeeVee

15

9

我曾经遇到过相同的问题,这个不太规范的修复方法解决了它,至少在我找到更好的方法之前可以这么用。

    public ActionResult Login(string returnUrl)
    {
        if (AuthenticationManager.User.Identity.IsAuthenticated)
        {
            AuthenticationManager.SignOut();
            return RedirectToAction("Login");
        }

...


2

我在生产服务器上大部分时间都会遇到相同的异常。

为什么会出现这种情况?

当用户使用有效凭据登录,一旦登录并重定向到另一个页面,然后按下后退按钮,将显示登录页面,再次输入有效凭据时,就会出现此异常。

如何解决?

只需添加此行代码即可完美运作,不会出现错误。

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")]

1
[OutputCache(NoStore=true, Duration=0, VaryByParam="None")]

public ActionResult Login(string returnUrl)

您可以通过在登录(Get)操作的第一行上设置断点来进行测试。在添加OutputCache指令之前,断点会在第一次加载时被触发,但是在单击浏览器后退按钮后不会触发。添加指令后,每次都应该触发断点,因此AntiForgeryToken将是正确的,而不是空的。

1

在注册过程中,我遇到了一个相对具体但类似的问题。一旦用户点击发送给他们的电子邮件链接,他们将被登录并直接发送到一个帐户详细信息屏幕以填写更多信息。我的代码如下:

    Dim result = Await UserManager.ConfirmEmailAsync(userId, code)
    If result.Succeeded Then
        Dim appUser = Await UserManager.FindByIdAsync(userId)
        If appUser IsNot Nothing Then
            Dim signInStatus = Await SignInManager.PasswordSignInAsync(appUser.Email, password, True, shouldLockout:=False)
            If signInStatus = SignInStatus.Success Then
                Dim identity = Await UserManager.CreateIdentityAsync(appUser, DefaultAuthenticationTypes.ApplicationCookie)
                AuthenticationManager.SignIn(New AuthenticationProperties With {.IsPersistent = True}, identity)
                Return View("AccountDetails")
            End If
        End If
    End If

我发现Return View("AccountDetails")给了我Token异常,我猜测是因为ConfirmEmail函数被AllowAnonymous修饰,但AccountDetails函数有ValidateAntiForgeryToken。
将Return更改为Return RedirectToAction("AccountDetails")解决了我的问题。

0

我在一个单页ASP.NET MVC Core应用程序中遇到了同样的问题。我通过在所有更改当前身份声明的控制器操作中设置HttpContext.User来解决了这个问题(因为MVC仅对后续请求执行此操作,如此处所述)。我使用了结果过滤器而不是中间件来将防伪cookie附加到我的响应中,这确保它们只在MVC操作返回后生成。

控制器(注意:我正在使用ASP.NET Core Identity管理用户):

[Authorize]
[ValidateAntiForgeryToken]
public class AccountController : Controller
{
    private SignInManager<IdentityUser> signInManager;
    private UserManager<IdentityUser> userManager;
    private IUserClaimsPrincipalFactory<IdentityUser> userClaimsPrincipalFactory;

    public AccountController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory)
    {
        this.signInManager = signInManager;
        this.userManager = userManager;
        this.userClaimsPrincipalFactory = userClaimsPrincipalFactory;
    }

    [HttpPost]
    [AllowAnonymous]
    public async Task<IActionResult> Login(string username, string password)
    {
        if (username == null || password == null)
        {
            return BadRequest(); // Alias of 400 response
        }

        var result = await signInManager.PasswordSignInAsync(username, password, false, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            var user = await userManager.FindByNameAsync(username);

            // Must manually set the HttpContext user claims to those of the logged
            // in user. Otherwise MVC will still include a XSRF token for the "null"
            // user and token validation will fail. (MVC appends the correct token for
            // all subsequent reponses but this isn't good enough for a single page
            // app.)
            var principal = await userClaimsPrincipalFactory.CreateAsync(user);
            HttpContext.User = principal;

            return Json(new { username = user.UserName });
        }
        else
        {
            return Unauthorized();
        }
    }

    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        await signInManager.SignOutAsync();

        // Removing identity claims manually from the HttpContext (same reason
        // as why we add them manually in the "login" action).
        HttpContext.User = null;

        return Json(new { result = "success" });
    }
}

结果过滤器以附加防伪 cookie:

public class XSRFCookieFilter : IResultFilter
{
    IAntiforgery antiforgery;

    public XSRFCookieFilter(IAntiforgery antiforgery)
    {
        this.antiforgery = antiforgery;
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        var HttpContext = context.HttpContext;
        AntiforgeryTokenSet tokenSet = antiforgery.GetAndStoreTokens(context.HttpContext);
        HttpContext.Response.Cookies.Append(
            "MyXSRFFieldTokenCookieName",
            tokenSet.RequestToken,
            new CookieOptions() {
                // Cookie needs to be accessible to Javascript so we
                // can append it to request headers in the browser
                HttpOnly = false
            } 
        );
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {

    }
}

Startup.cs 提取:

public partial class Startup
{
    public Startup(IHostingEnvironment env)
    {
        //...
    }

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {

        //...

        services.AddAntiforgery(options =>
        {
            options.HeaderName = "MyXSRFFieldTokenHeaderName";
        });


        services.AddMvc(options =>
        {
            options.Filters.Add(typeof(XSRFCookieFilter));
        });

        services.AddScoped<XSRFCookieFilter>();

        //...
    }

    public void Configure(
        IApplicationBuilder app,
        IHostingEnvironment env,
        ILoggerFactory loggerFactory)
    {
        //...
    }
}

-3
在互联网商店中,反伪造令牌验证存在问题:用户打开许多标签页(带有商品),在一个标签页上登录后,尝试在另一个标签页上登录,会出现AntiForgeryException异常。因此,AntiForgeryConfig.SuppressIdentityHeuristicChecks = true对我没有帮助,所以我使用了这种丑陋的hackfix,也许对某些人有帮助:
   public class ExceptionPublisherExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext exceptionContext)
    {
        var exception = exceptionContext.Exception;

        var request = HttpContext.Current.Request;
        if (request != null)
        {
            if (exception is HttpAntiForgeryException &&
                exception.Message.ToLower().StartsWith("the provided anti-forgery token was meant for user \"\", but the current user is"))
            {
                var isAjaxCall = string.Equals("XMLHttpRequest", request.Headers["x-requested-with"], StringComparison.OrdinalIgnoreCase);
                var returnUrl = !string.IsNullOrWhiteSpace(request["returnUrl"]) ? request["returnUrl"] : "/";
                var response = HttpContext.Current.Response;

                if (isAjaxCall)
                {
                    response.Clear();
                    response.StatusCode = 200;
                    response.ContentType = "application/json; charset=utf-8";
                    response.Write(JsonConvert.SerializeObject(new { success = 1, returnUrl = returnUrl }));
                    response.End();
                }
                else
                {
                    response.StatusCode = 200;
                    response.Redirect(returnUrl);
                }
            }
        }


        ExceptionHandler.HandleException(exception);
    }
}

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new ExceptionPublisherExceptionFilter());
        filters.Add(new HandleErrorAttribute());
    }
}

如果可以设置生成反伪造令牌的选项,以排除用户名或类似的内容,那将是非常棒的。


12
这是一个处理问题的可怕范例。不要使用它。 - xxbbcc
好的,使用案例:带有防伪令牌的登录表单。在两个浏览器选项卡中打开它。首先在第一个选项卡中登录。您无法刷新第二个选项卡。您建议采取什么解决方案,以便用户尝试从第二个选项卡登录时具有正确的行为? - user3364244
@user3364244:正确的行为可以通过使用WebSockets或SignalR检测外部登录。这是同一个会话,所以我想你可以让它工作 :-) - dampee

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