AntiForgeryToken 到期空白页

AntiForgeryToken Expiration Blank Page

我在 ASP.NET Core 2.2 中使用 IdentityServer4。在 Post 登录方法上,我应用了 ValidateAntiForgeryToken。通常在登录页面上停留 20 分钟到 2 小时后,然后尝试登录会生成一个空白页面。

如果您查看 Postman 控制台,您会收到一条 400 Bad Request 消息。然后,我将 AntiForgery 选项上的 Cookie 过期时间设置为 90 天。我能够让该页面最多停留 6 小时并仍然登录。然而,大约 8 小时后(一夜之间),我在尝试登录后再次收到空白页面。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login
services.AddAntiforgery(options =>
{
    options.Cookie.Expiration = TimeSpan.FromDays(90);
});

我希望能够在登录页面上停留 90 天,这是 cookie 的持续时间,但这不起作用。我如何让 AntiforgeryToken 的 cookie 持续整个 90 天或我设置的任何时间,而不是超时或过期?有没有办法捕获此错误并将用户重定向回登录方法?

这是我的最终解决方案。我使用 IAntifogery 依赖注入添加了一个属性。

public class CustomValidationAttribute : ActionFilterAttribute
{
    private IAntiforgery _antiForgery { get; }

    public CustomValidationAttribute(IAntiforgery antiforgery)
    {
        _antiForgery = antiforgery;
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var isRequestValid = await this._antiForgery.IsRequestValidAsync(context.HttpContext);
        if (!isRequestValid)
        {
            //Add Code here if token is not valid

            return;         
        }

        await next();
    }
}

将属性添加到也使用 [HttpPost] 的控制器方法

[TypeFilter(typeof(CustomValidationAttribute))]

2021 年更新

自 ASP.Net Core 3.0 MS decided to make ValidateAntiforgeryTokenAuthorizationFilter internal. Now we have to copy-paste their code, to be able to derive. But most likely we don't need to. To just change the resulting behavior all we need is to test the context for the IAntiforgeryValidationFailedResult and proceed accordantly, as described in this example

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Core.Infrastructure;
using Microsoft.AspNetCore.Mvc.Filters;

namespace BasicWebSite.Filters
{
    public class RedirectAntiforgeryValidationFailedResultFilter : IAlwaysRunResultFilter
    {
        public void OnResultExecuting(ResultExecutingContext context)
        {
            if (context.Result is IAntiforgeryValidationFailedResult result)
            {
                context.Result = 
                    new RedirectResult("http://example.com/antiforgery-redirect");
            }
        }

        public void OnResultExecuted(ResultExecutedContext context)
        { }
    }
}

然后在控制器内:

// POST: /Antiforgery/LoginWithRedirectResultFilter
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[TypeFilter(typeof(RedirectAntiforgeryValidationFailedResultFilter))]
public string LoginWithRedirectResultFilter(LoginViewModel model)
{
    return "Ok";
}

涵盖 .net core 2.2 的原始答案

另一种使用默认实现的实现,包括所有预检查、日志记录等。它仍然是 AuthorizationFilter,因此可以防止执行任何进一步的操作。唯一的区别是它触发 HttpGet 到相同的 url 而不是默认的 400 响应,一种 Post/Redirect/Get 模式实现。

public class AnotherAntiForgeryTokenAttribute : TypeFilterAttribute
{
    public AnotherAntiForgeryTokenAttribute() : base(typeof(AnotherAntiforgeryFilter))
    {
    }
}


public class AnotherAntiforgeryFilter:ValidateAntiforgeryTokenAuthorizationFilter,
    IAsyncAuthorizationFilter
{
    public AnotherAntiforgeryFilter(IAntiforgery a, ILoggerFactory l) : base(a, l)
    {
    }

    async Task IAsyncAuthorizationFilter.OnAuthorizationAsync(
        AuthorizationFilterContext ctx)
    {
        await base.OnAuthorizationAsync(ctx);

        if (ctx.Result is IAntiforgeryValidationFailedResult)
        {
            // the next four rows are optional, just illustrating a way
            // to save some sensitive data such as initial query
            // the form has to support that
            var request = ctx.HttpContext.Request;
            var url = request.Path.ToUriComponent();
            if (request.Form?["ReturnUrl"].Count > 0)
                url = $"{url}?ReturnUrl={Uri.EscapeDataString(request.Form?["ReturnUrl"])}";

            // and the following is the only real customization
            ctx.Result = new LocalRedirectResult(url);
        }
    }
}

对 d_f 代码稍作修改 我们只是将错误添加到 ModelState,而不是页面重定向。然后我们在模型状态摘要中显示。

public class CustomAntiForgeryTokenAttribute : TypeFilterAttribute
{
    public CustomAntiForgeryTokenAttribute() : base(typeof(AnotherAntiforgeryFilter))
    {
    }
}


public class AnotherAntiforgeryFilter : ValidateAntiforgeryTokenAuthorizationFilter,
    IAsyncAuthorizationFilter
{
    public AnotherAntiforgeryFilter(IAntiforgery a, ILoggerFactory l) : base(a, l)
    {
    }

    async Task IAsyncAuthorizationFilter.OnAuthorizationAsync(
        AuthorizationFilterContext ctx)
    {
        await base.OnAuthorizationAsync(ctx);

        if (ctx.Result is IAntiforgeryValidationFailedResult)
        {
            ctx.ModelState.AddModelError("Token", "Validation Token Expired. Please try again");
            ctx.Result = null;

        }
    }
}

我们正在使用 .netcore 3.1/razor pages 并希望将错误放入模型状态而不是 400 错误请求。从 Rodney 和 Petr J 的回答中编辑我们首先全局禁用验证(因为默认情况下自动验证处于启用状态)然后使用页面过滤器手动验证。不是最有效的,但感觉比尝试捕获和还原 AntiforgeryValidationFailedResult.

更直接

配置服务:

services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            // Note: This seems to apply globally to all razor areas vs .AddRazorPages()
            
            // First remove default validation which returns 400 Bad Request
            //https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0#overriding-the-default-order
            options.Filters.Add(new Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute() { Order = 1001 });

            // Then add back a page filter, runs on all pages and areas (not tested on anything except razor pages; confirm odata, api, mvc)
            // Option 1 Type activated: instance for each request, DI
            // https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0#dependency-injection
            options.Filters.Add(typeof(Filters.AntiforgeryValidationModelStateOverridePageFilter));
            
        })
            

然后自定义过滤器:

public class AntiforgeryValidationModelStateOverridePageFilter : IAsyncPageFilter {

    private readonly IAntiforgery _defaultAntiforgery;
    public AntiforgeryValidationModelStateOverridePageFilter(IAntiforgery defaultAntiforgery)
    {
        _defaultAntiforgery = defaultAntiforgery;
    }

    public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    {
        var isRequestValid = _defaultAntiforgery.IsRequestValidAsync(context.HttpContext).Result;

        if (!isRequestValid)
        {
            //Add Code here if token is not valid
            context.ModelState.AddModelError("", Data.Constants.UserMessages.UserMessageErrorAntiforgery);
        }

        return Task.CompletedTask;
    }

    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,
                                                  PageHandlerExecutionDelegate next)
    {
        // Do post work.
        await next.Invoke();
    }
}