【问题标题】:Anti forgery token is meant for user "" but the current user is "username"防伪令牌适用于用户“”,但当前用户是“用户名”
【发布时间】:2013-02-04 21:05:28
【问题描述】:

我正在构建一个单页应用程序,但遇到了防伪令牌问题。

我知道为什么会出现这个问题,但我不知道如何解决它。

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

  1. 未登录用户加载对话框(使用生成的防伪令牌)
  2. 用户关闭对话框
  3. 用户登录
  4. 用户打开同一个对话框
  5. 用户在对话框中提交表单

防伪令牌适用于用户“”,但当前用户是 “用户名”

发生这种情况的原因是因为我的应用程序是 100% 单页的,当用户通过 ajax 帖子成功登录到 /Account/JsonLogin 时,我只需使用从服务器,但不重新加载页面。

我知道这是原因,因为如果我在第 3 步和第 4 步之间简单地重新加载页面,就没有错误。

因此,在重新加载页面之前,加载表单中的@Html.AntiForgeryToken() 似乎仍会为旧用户返回一个令牌。

如何更改 @Html.AntiForgeryToken() 以为新的经过身份验证的用户返回令牌?

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

【问题讨论】:

  • 你真的可以验证@Html.AntiForgeryToken 代码在没有重新加载的情况下被调用了吗?
  • 绝对是,我可以成功地打破那里检查 HttpContext.Current.User 对象,就像我提到的那样
  • @parliament 您能否在下面的答案中告诉您您选择了哪个选项。
  • 如果我没记错的话,我相信我做了一个例外来完全重新加载。但我希望很快在一个新项目中遇到这个问题。如果我选择更好的工作选项,我会回复。

标签: asp.net-mvc-4 membership-provider


【解决方案1】:

发生这种情况是因为防伪令牌将用户的用户名作为加密令牌的一部分嵌入,以便更好地进行验证。当您第一次调用@Html.AntiForgeryToken() 时,用户未登录,因此令牌将有一个空字符串作为用户名,在用户登录后,如果您不替换防伪令牌,它将不会通过验证,因为初始令牌用于匿名用户,现在我们有了一个具有已知用户名的经过身份验证的用户。

你有几个选择来解决这个问题:

  1. 就在这一次让您的 SPA 进行完整的 POST,当页面重新加载时,它将有一个嵌入了更新用户名的防伪令牌。

  2. 仅查看 @Html.AntiForgeryToken() 的部分视图,并在登录后立即执行另一个 AJAX 请求并将您现有的防伪令牌替换为请求的响应。

请注意,设置AntiForgeryConfig.SuppressIdentityHeuristicChecks = true 不会禁用用户名验证,它只会更改验证的工作方式。请参阅ASP.NET MVC docs、读取该属性的source code,以及验证令牌中的用户名的source code,无论该配置的值如何。

【讨论】:

  • @parliament:您接受了这个答案,您能与我们分享您选择的选项吗?
  • +1 表示不错且简单的选项 3。OAuth 提供商的定时注销也会导致此问题。
  • 选项 3 对我不起作用。注销时,我在登录页面上打开了两个窗口。在一个窗口中作为一个用户登录,然后在另一个窗口中作为另一个用户登录并收到相同的错误。
  • 不幸的是,我无法找到一个好的解决方案。我从登录页面中删除了令牌。登录后我仍然将其包含在帖子中。
  • 选项 3 对我也不起作用。仍然出现同样的错误。
【解决方案2】:
[OutputCache(NoStore=true, Duration=0, VaryByParam="None")]

public ActionResult Login(string returnUrl)

您可以通过在登录(获取)操作的第一行放置一个断点来测试这一点。在添加 OutputCache 指令之前,断点将在第一次加载时命中,但在单击浏览器后退按钮后不会。添加指令后,您应该每次都会遇到断点,因此 AntiForgeryToken 将是正确的,而不是空的。

【讨论】:

    【解决方案3】:

    我在生产服务器上大部分时间都发生了同样的异常。

    为什么会这样?

    当用户使用有效凭据登录并且一旦登录并重定向到另一个页面时会发生这种情况,并且在他们按下后退按钮将显示登录页面并且他再次输入有效凭据时会发生此异常。

    如何解决?

    只需添加此行即可完美运行,不会出错。

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

    【讨论】:

      【解决方案4】:

      当您已通过身份验证后登录时会显示该消息。

      这个 Helper 做的事情和[ValidateAntiForgeryToken] 属性完全一样。

      System.Web.Helpers.AntiForgery.Validate()
      

      从控制器中删除 [ValidateAntiForgeryToken] 属性并将此帮助器放入操作方法中。

      所以当用户已经通过身份验证时,重定向到主页,否则在验证后继续验证有效的防伪令牌。

      if (User.Identity.IsAuthenticated)
      {
          return RedirectToAction("Index", "Home");
      }
      
      System.Web.Helpers.AntiForgery.Validate();
      

      要尝试重现错误,请执行以下操作: 如果您在登录页面上并且未通过身份验证。如果您复制选项卡并使用第二个选项卡登录。 如果您返回登录页面上的第一个选项卡并尝试在不重新加载页面的情况下登录...您会遇到此错误。

      【讨论】:

      • 优秀的解决方案!在尝试了许多其他不起作用的建议后,这解决了我的问题。首先,重现错误很痛苦,直到我发现这可能是因为 2 个浏览器或选项卡打开同一个页面,并且用户从一个登录,然后从第二个登录而没有重新加载。
      • 感谢您的解决方案。也为我工作。我添加了一个检查以查看身份是否与登录用户名相同,如果是,我很乐意继续尝试登录用户,如果不是,则将其注销。例如,尝试 { System.Web.Helpers.AntiForgery.Validate();} catch (HttpAntiForgeryException) { if (!User.Identity.IsAuthenticated || string.Compare(User.Identity.Name, model.Username) != 0) { // 你的注销逻辑在这里 } }
      • 希望你不介意,我使用了这个答案并将它与我发现的其他一些逻辑结合起来(以不同的人身份登录)来回答一个较旧的问题:stackoverflow.com/a/68685999/555798。感谢您的帮助!
      【解决方案5】:

      我的应用程序经常出现这种情况,所以我决定用谷歌搜索它!

      我找到了关于这个错误的简单解释! 用户正在双击登录按钮!您可以在下面的链接中看到另一个用户在谈论这个问题:

      MVC 4 provided anti-forgery token was meant for user "" but the current user is "user"

      希望对你有帮助! =)

      【讨论】:

        【解决方案6】:

        我在单页 ASP.NET MVC Core 应用程序中遇到了同样的问题。我通过在所有更改当前身份声明的控制器操作中设置HttpContext.User 来解决它(因为MVC 仅对后续请求执行此操作,如here 所讨论的)。我使用结果过滤器而不是中间件将防伪 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)
            {
                //...
            }
        }
        

        【讨论】:

          【解决方案7】:

          我遇到了同样的问题,这个肮脏的 hack 得到了修复,至少在我能以更干净的方式修复它之前。

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

          ...

          【讨论】:

            【解决方案8】:

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

                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") 给了我令牌异常,我猜是因为 ConfirmEmail 函数用 AllowAnonymous 装饰,但 AccountDetails 函数有 ValidateAntiForgeryToken。

            将 Return 更改为 Return RedirectToAction("AccountDetails") 为我解决了这个问题。

            【讨论】:

              【解决方案9】:

              要修复错误,您需要将OutputCache 数据注释放在登录页面的获取ActionResult 上:

              [OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")] 
              public ActionResult Login(string returnUrl)
              

              【讨论】:

              • 我的用例是用户尝试登录,并显示错误,例如通过 ModelState.AddError() “禁用帐户”。然后,如果他们再次单击登录,他们将看到此错误。然而,这个修复只是再次给了他们一个空白的全新登录视图,而不是防伪令牌错误。所以,不是解决办法。
              • 我的案例: 1. 用户登录()并登陆主页。 2. 用户点击返回按钮并返回登录视图。 3. 用户再次登录并看到错误“Anti forgery token is means for user “” but current user is “username” 在错误页面上如果用户单击菜单中的任何其他选项卡,则应用程序按预期工作.使用上面的代码用户仍然可以点击返回按钮,但它会被重定向到主页。因此,无论用户点击多少次后退按钮,它都会将其重定向到主页。谢谢
              • 任何想法为什么这在 Xamarin webview 上不起作用?
              【解决方案10】:

              在网上商店中的防伪令牌验证存在问题:用户打开许多标签(带有商品)并在登录后尝试登录另一个并得到这样的 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());
                  }
              }
              

              如果可以设置防伪令牌生成选项,排除用户名或类似的东西,那就太好了。

              【讨论】:

              • 这是处理问题中问题的一个糟糕例子。不要使用这个。
              • 好的,用例:带有防伪令牌的登录表单。在 2 个浏览器选项卡中打开它。先登录。你不能刷新第二个标签。对于尝试从第二个选项卡登录的用户,您建议采取什么解决方案来获得正确的行为?
              • @user3364244:正确的行为可以是:使用 websockets 或 signalR 检测外部登录。这是同一个会话,所以我猜你可以让它工作:-)
              猜你喜欢
              • 2013-10-06
              • 2015-02-01
              • 2016-03-19
              • 2013-12-18
              • 2015-01-14
              • 2013-04-30
              • 1970-01-01
              • 2015-12-15
              • 2018-08-01
              相关资源
              最近更新 更多