単一ページのアプリケーションを構築しているときに、偽造防止トークンの問題が発生しています。
私は問題が発生する理由を知っていますが、それを修正する方法がわかりません。
次が発生するとエラーが発生します。
偽造防止トークンはユーザー「」を対象としていますが、現在のユーザーは「username」です
これが発生する理由は、アプリケーションが100%シングルページであり、ユーザーが/Account/JsonLogin
へのajaxポストを介して正常にログインすると、サーバーから返された「認証済みビュー」で現在のビューを切り替えるだけです。しかしページをリロードしないでください。
これが理由であることがわかります。ステップ3と4の間でページを単純にリロードすればエラーは発生しないからです。
そのため、ロードされたフォームの@Html.AntiForgeryToken()
は、ページがリロードされるまで古いユーザーのトークンを返しているようです。
認証済みの新しいユーザーのトークンを返すように@Html.AntiForgeryToken()
を変更するにはどうすればよいですか?
すべてのApplication_AuthenticateRequest
にカスタムGenericalPrincipal
を付けて新しいIIdentity
を挿入するので、@Html.AntiForgeryToken()
が呼び出されるまでにHttpContext.Current.User.Identity
が実際にIsAuthenticated
プロパティがtrueに設定されているにもかかわらず、@Html.AntiForgeryToken
は、ページのリロードを行わない限り、古いユーザーのトークンをまだレンダリングしているようです。
これは、偽造防止トークンがユーザーのユーザー名を暗号化されたトークンの一部として埋め込み、検証を向上させるために発生しています。 @Html.AntiForgeryToken()
を最初に呼び出したとき、ユーザーはログインしていないため、ユーザーがログインした後、トークンはユーザー名に空の文字列を持ちます。偽造防止トークンを置き換えないと、検証に合格しません。最初のトークンは匿名ユーザー用でしたが、現在は既知のユーザー名を持つ認証済みユーザーがいます。
この問題を解決するには、いくつかのオプションがあります。
今回は、SPAに完全なPOSTを実行させ、ページがリロードされると、更新されたユーザー名が埋め込まれた偽造防止トークンが作成されます。
@Html.AntiForgeryToken()
だけの部分ビューを用意し、ログイン直後に別のAJAXリクエストを実行し、既存の偽造防止トークンをリクエストの応答に置き換えます。
偽造防止の検証が実行するIDチェックを無効にするだけです。以下をApplication_Startメソッドに追加します:AntiForgeryConfig.SuppressIdentityHeuristicChecks = true
。
このエラーを修正するには、ログインページのOutputCache
の取得にActionResult
データ注釈を配置する必要があります。
[OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")]
public ActionResult Login(string returnUrl)
私のアプリケーションでは何度も起こりますので、グーグルで検索することにしました!
このエラーについて簡単な説明を見つけました!ユーザーはログイン用のボタンをダブルクリックしています!以下のリンクで別のユーザーがそれについて話しているのを見ることができます:
MVC 4が提供する偽造防止トークンはユーザー ""向けでしたが、現在のユーザーは "user"です
私はそれが役立つことを願っています! =)
私は同じ問題を抱えていましたが、少なくとも汚い方法で修正できるまでは、この汚いハックで修正されました。
public ActionResult Login(string returnUrl)
{
if (AuthenticationManager.User.Identity.IsAuthenticated)
{
AuthenticationManager.SignOut();
return RedirectToAction("Login");
}
...
実稼働サーバーでほとんどの場合に同じ例外が発生します。
なぜそれが起こるのですか?
ユーザーが有効な資格情報でログインし、ログインして別のページにリダイレクトし、戻るボタンを押した後にログインページが表示され、再びこの例外が発生する有効な資格情報を入力すると発生します。
解決方法?
この行を追加するだけで完璧に機能し、エラーは発生しません。
[OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")]
すでに認証されているときにログインすると、メッセージが表示されます。
このヘルパーは、[ValidateAntiForgeryToken]
属性とまったく同じことを行います。
System.Web.Helpers.AntiForgery.Validate()
コントローラーから[ValidateAntiForgeryToken]
属性を削除し、このヘルパーをアクションメソッドに配置します。
そのため、ユーザーが既に認証されている場合、ホームページにリダイレクトするか、そうでない場合は、この検証後に有効な偽造防止トークンの検証を続行します。
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Home");
}
System.Web.Helpers.AntiForgery.Validate();
エラーの再現を試みるには、次の手順を実行します。ログインページにアクセスしていて、認証されていない場合。タブを複製し、2番目のタブでログインした場合。ログインページの最初のタブに戻って、ページをリロードせずにログインしようとすると、このエラーが発生します。
登録プロセスの中で、かなり具体的でありながら同様の問題がありました。ユーザーが送信された電子メールのリンクをクリックすると、ユーザーはログインしてアカウントの詳細画面に直接送信され、さらに情報が入力されます。私のコードは:
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")に変更すると、問題が解決しました。
[OutputCache(NoStore=true, Duration=0, VaryByParam="None")]
public ActionResult Login(string returnUrl)
これをテストするには、ログイン(取得)アクションの最初の行にブレークポイントを配置します。 OutputCacheディレクティブを追加する前に、最初のロードでブレークポイントにヒットしますが、ブラウザーの戻るボタンをクリックした後はヒットしません。ディレクティブを追加した後、毎回ブレークポイントがヒットするようになります。そのため、AntiForgeryTokenは空ではなくcorectになります。
単一ページのASP.NET MVCコアアプリケーションでも同じ問題が発生しました。現在のID要求を変更するすべてのコントローラーアクションでHttpContext.User
を設定することで解決しました(MVCは here で説明されているように、後続の要求に対してのみこれを行うため)。ミドルウェアの代わりに結果フィルターを使用して、偽造防止Cookieを応答に追加し、MVCアクションが返された後にのみ生成されるようにしました。
コントローラー(注意。ASP.NETCore 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)
{
//...
}
}