反伪造令牌用于用户“”,但当前用户是“用户名”


130

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

我知道为什么会发生此问题,我只是不知道如何解决。

当发生以下情况时出现错误:

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

反伪造令牌用于用户“”,但当前用户是“用户名”

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

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

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

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

我在每次调用时都注入了一个GenericalPrincipal带有自定义的新变量,实际上,我的自定义Identity 属性的属性设置为true,但除非我重新加载页面,否则它似乎仍为旧用户呈现令牌。IIdentityApplication_AuthenticateRequest@Html.AntiForgeryToken()HttpContext.Current.User.IdentityIsAuthenticated@Html.AntiForgeryToken


您真的可以验证@ Html.AntiForgeryToken代码是否在不重新加载的情况下被调用吗?
Kyle C

绝对是,我可以成功中断检查HttpContext.Current.User对象的能力,就像我提到的那样
国会


@parliament您能否在下面的答案中告诉您选择了哪个选项。
Siddharth Pandey 2014年

如果我没记错的话,我相信我有例外进行完整的重新加载。但是我希望在一个新项目中很快遇到这个问题。如果我选择了更好的工作方式,则会回发。
议会

Answers:


170

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

您可以选择几种方法来解决此问题:

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

  2. @Html.AntiForgeryToken()登录后立即进行局部查看,执行另一个AJAX请求,并用请求的响应替换您现有的防伪令牌。

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


21
@parliament:您接受了这个答案,能否与我们分享您选择了哪个选项?
R. Schreurs

9
+1表示美观和简单的选项3。OAuth提供者的定时注销也会导致此问题。
Gone Coding

18
选项3对我不起作用。注销时,我在登录页面上打开了两个窗口。在一个窗口中以一个用户身份登录,然后在另一个窗口中以另一用户身份登录并收到相同的错误。
McGaz 2014年

5
不幸的是,我对此没有一个好的解决方案。我从登录页面删除了令牌。登录后我仍将其包含在帖子中。
麦加兹2014年

7
选项3也不适合我。仍然出现相同的错误。
Joao Leme 2014年

25

要解决该错误,您需要将“ OutputCache数据注释”放置在“获取ActionResult登录名”页面上,如下所示:

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

3
这对我来说解决了这个问题,完全有意义。谢谢!
Prime03

我的用例是用户尝试登录,并通过ModelState.AddError()显示了一个错误,例如“帐户被禁用”。然后,如果他们再次单击登录,他们将看到此错误。但是,此修复程序只是再次为他们提供了空白的全新登录视图,而不是反伪造令牌错误。因此,不是解决方法。
yourpublicdisplayname

我的情况:1.用户LogIn()并进入主页。2.用户单击“后退”按钮并返回“登录”视图。3.再次登录用户,并看到错误“反伪造令牌是为用户“”提供的,但当前用户为“用户名””。在错误页面上,如果用户单击菜单上的任何其他选项卡,则表明应用程序正在按预期方式工作。使用上面的代码,用户仍然可以单击“后退”按钮,但是它将被重定向到主页。因此,无论用户单击后退按钮多少次,它都会将其重定向到主页。谢谢
拉维

有什么想法为什么在Xamarin Webview上不起作用?
Noobie3001 '19

1
有关完整说明,请参见通过输出缓存提高性能
混合式(Stomy)


8

我遇到了同样的问题,这个肮脏的hack至少在我可以更干净的方式解决之前就将其修复。

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

...


1
好像我遇到了同样的问题。IMO并不是黑客,这是我们登录时都应检查的更常见的事情。如果用户已经登录,则将其注销并显示登录页面。解决了我的问题,谢谢。
亚历山大

7

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

该助手与[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();}抓住(HttpAntiForgeryException){if(!User.Identity.IsAuthenticated || string.Compare(User.Identity.Name,model.Username)!= 0) {//您的注销逻辑在这里}}
Steve Owen

2

在大多数情况下,生产服务器上都会发生相同的异常。

为什么会发生?

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

怎么解决?

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

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

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。

更改Return to Return RedirectToAction(“ AccountDetails”)对我来说解决了这个问题。


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

public ActionResult Login(string returnUrl)

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


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

-2

Internet商店中的防伪令牌验证存在问题:用户打开许多选项卡(带有商品),并且在登录后尝试登录另一个并获得了这样的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 2014年

完全同意xxbbcc。
哈维尔2014年

好,用例:带有防伪令牌的登录表单。在2个浏览器标签中将其打开。首先登录。您无法刷新第二个标签。对于尝试从第二个选项卡登录的用户,您建议采用哪种解决方案以使其行为正确?
user3364244 2014年

@ user3364244:可以通过以下正确行为:使用websockets或signalR检测外部登录。这是同一会话,所以我想您可以使它起作用:-)
阻尼器2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.