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

时间:2013-02-20 00:16:10

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

我正在构建单页应用程序并遇到防伪令牌问题。

我知道为什么问题发生我只是不知道如何修复它。

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

  1. 非登录用户加载对话框(带有生成的防伪标记)
  2. 用户关闭对话框
  3. 用户登录
  4. 用户打开相同的对话框
  5. 用户在对话框中提交表单
  6.   

    防伪标记适用于用户“”,但当前用户是   “用户名”

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

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

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

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

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

10 个答案:

答案 0 :(得分:163)

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

您有几个选项可以解决此问题:

  1. 就在这个时候让你的SPA做一个完整的POST,当页面重新加载时,它会有一个防伪标记,其中嵌入了更新的用户名。

  2. 只有@Html.AntiForgeryToken()的部分视图,登录后立即执行,执行另一个AJAX请求,并将现有的防伪标记替换为请求的响应。

  3. 只需禁用防伪验证执行的身份检查即可。将以下内容添加到 Application_Start 方法中: AntiForgeryConfig.SuppressIdentityHeuristicChecks = true

答案 1 :(得分:20)

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

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

答案 2 :(得分:9)

我的应用程序发生了很多次,所以我决定谷歌吧!

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

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

我希望它有所帮助! =)

答案 3 :(得分:6)

我遇到了同样的问题,这个肮脏的黑客修复了它,至少在我能以更清洁的方式修复它之前。

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

...

答案 4 :(得分: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”)给了我令牌异常,我猜是因为ConfirmEmail函数是用AllowAnonymous装饰的,但AccountDetails函数有ValidateAntiForgeryToken。

更改返回的返回值RedirectToAction(“AccountDetails”)为我解决了这个问题。

答案 5 :(得分:1)

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

public ActionResult Login(string returnUrl)

您可以通过在Login(Get)操作的第一行放置一个断点来测试这一点。在添加OutputCache指令之前,断点将在第一次加载时被点击,但是在单击浏览器后退按钮之后它不会。添加指令后,您最终应该每次都会遇到断点,因此AntiForgeryToken将是核心的,而不是空的。

答案 6 :(得分:1)

我在生产服务器上大多数时间都遇到相同的例外情况。

为什么会发生?

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

如何解决?

只需添加此行即可完美运行,没有错误。

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

答案 7 :(得分:0)

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

控制器(注意:我使用ASP.NET核心身份管理用户):

[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 extract:

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)
    {
        //...
    }
}

答案 8 :(得分:0)

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

此助手与[ValidateAntiForgeryToken]属性完全相同。

System.Web.Helpers.AntiForgery.Validate()

从控制器中删除[ValidateAntiForgeryToken] attribut并将此帮助程序置于操作方法中。

因此,当用户已经过身份验证时,重定向到主页,或者如果在此验证后没有继续验证有效的防伪令牌,那么。

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

System.Web.Helpers.AntiForgery.Validate();
  

要尝试重现错误,请按以下步骤操作:   如果您在登录页面上并且未经过身份验证。如果复制选项卡,则使用第二个选项卡登录。   如果你回到登录页面上的第一个标签,你尝试登录而不重新加载页面......你有这个错误。

答案 9 :(得分:-2)

在互联网商店中存在防伪标记验证问题:用户打开许多标签(带有商品),并在登录后尝试登录另一个并获得此类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());
    }
}

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