137

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

我知道为什么会出现问题,只是不知道如何解决。

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

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

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

发生这种情况的原因是因为我的应用程序是 100% 单页的,当用户通过 ajax 帖子成功/Account/JsonLogin登录到页。

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

所以看起来@Html.AntiForgeryToken()在加载的表单中仍然会为旧用户返回一个令牌,直到页面重新加载。

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

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

4

10 回答 10

172

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

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

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

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

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

于 2013-03-25T13:09:44.017 回答
26

要修复该错误,您需要将OutputCacheData Annotation 放在 Get ActionResultof Login 页面上,如下所示:

[OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")] 
public ActionResult Login(string returnUrl)
于 2014-11-10T06:35:41.210 回答
16

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

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

MVC 4 提供的防伪令牌用于用户“”,但当前用户是“用户”

我希望它有帮助!=)

于 2015-06-23T15:04:09.320 回答
16

当您已经通过身份验证后登录时会出现该消息。

[ValidateAntiForgeryToken]这个 Helper 的作用与属性完全相同。

System.Web.Helpers.AntiForgery.Validate()

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

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

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

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

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

于 2017-12-18T12:23:40.447 回答
9

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

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

...

于 2016-12-19T19:57:03.287 回答
2

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

为什么会这样?

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

怎么解决?

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

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")]
于 2018-09-06T12:36:23.177 回答
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 更改为 Return RedirectToAction("AccountDetails") 为我解决了这个问题。

于 2016-08-12T11:48:10.877 回答
1
[OutputCache(NoStore=true, Duration=0, VaryByParam="None")]

public ActionResult Login(string returnUrl)

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

于 2017-10-03T08:38:38.837 回答
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)
    {
        //...
    }
}
于 2017-07-25T09:10:18.380 回答
-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());
    }
}

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

于 2014-02-28T09:59:47.670 回答