防伪令牌是给用户的,但当前用户是“用户名”

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

我知道问题发生的原因,只是不知道如何解决。

当下列情况发生时,我得到了错误:

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

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

之所以会发生这种情况,是因为我的应用程序是100% 单页的,当用户通过 ajax 文章成功登录到 /Account/JsonLogin时,我只需用从服务器返回的“经过身份验证的视图”切换掉当前的视图,而不是 不要重新加载页面。

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

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

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

我在每个 Application_AuthenticateRequest上注入一个新的 GenericalPrincipal和一个定制的 IIdentity,所以当 @Html.AntiForgeryToken()被称为 HttpContext.Current.User.Identity的时候,事实上我的定制 Identity 和 IsAuthenticated属性被设置为 true,然而除非我重新加载页面,否则 @Html.AntiForgeryToken似乎仍然为老用户呈现一个令牌。

99553 次浏览

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

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

  1. 只是这一次让您的 SPA 做一个完整的 POST,当页面重新加载它将有一个防伪令牌与更新的用户名嵌入。

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

请注意,设置 AntiForgeryConfig.SuppressIdentityHeuristicChecks = true并不禁用用户名验证,它只是改变验证的工作方式。查看 NET MVC 文档、读取该属性的 源代码和验证令牌中的用户名的 源代码,而不管该配置的值如何。

在网上商店的防伪令牌验证有一个问题: 用户打开许多标签(商品)和登录后,一个尝试登录到另一个,并得到这样的防伪异常。 那么,防伪配置。SuppressIdentity 启发式检查 = true 对我没有帮助,所以我使用了这样丑陋的黑客修复程序,也许它会对某人有帮助:

   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());
}
}

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

要修复错误,您需要将 OutputCache数据注释放在 Get ActionResult of Login 页面上,如下所示:

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

这种情况在我的应用程序中经常发生,所以我决定谷歌一下!

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

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

我希望这能有所帮助! =)

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

    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”)给了我一个令牌异常,我猜测是因为 ConfirEmail 函数是用 AllowAnonymous 修饰的,而 AccountDetails 函数是用 ValidateAntiForgeryToken 修饰的。

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

我也有同样的问题,这个肮脏的黑客把它修好了,至少在我能用更干净的方式修好之前。

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

...

我在一个单页 ASP.NET MVC 核心应用程序中遇到了同样的问题。我通过在所有改变当前身份声明的控制器操作中设置 HttpContext.User来解决这个问题(因为 MVC 只对后续请求执行此操作,正如我们讨论的 给你)。我使用了一个结果过滤器而不是中间件来将防伪 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 摘录:

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)
{
//...
}
}
[OutputCache(NoStore=true, Duration=0, VaryByParam="None")]


public ActionResult Login(string returnUrl)

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

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

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

System.Web.Helpers.AntiForgery.Validate()

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

因此,当用户已经被认证,重定向到主页,或者如果不继续验证有效的防伪令牌后,此验证。

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


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

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

在生产服务器上,大多数时候都会出现同样的异常。

为什么会这样?

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

怎么解决?

只要添加这一行和工作完美,没有得到一个错误。

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