加入session支持

public void ConfigureServices(IServiceCollection services)
{
    // add session support
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => false;
        options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;
    });

    services.AddSession(options =>
    {
        options.IdleTimeout = TimeSpan.FromSeconds(100);
        options.Cookie.HttpOnly = true;
        options.Cookie.IsEssential = true;
    });

    // 没有下面这句,启动会失败,
    // 报这个错: InvalidOperationException: Unable to resolve service for type 'Microsoft.Extensions.Caching.Distributed.IDistributedCache' while attempting to activate 'Microsoft.AspNetCore.Session.DistributedSessionStore
    services.AddDistributedMemoryCache();
    // 因为PreventDoublePostAttribute用到了IAntiforgery,所以这里还要加
    services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
    ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
    ...
    app.UseStaticFiles();
    // add session pipepline
    app.UseSession();
    ...
}

引入PreventDoublePostAttribute属性

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class PreventDoublePostAttribute : ActionFilterAttribute
{
    private const string UniqFormuId = "LastProcessedToken";
    public override async void OnActionExecuting(ActionExecutingContext context)
    {
        IAntiforgery antiforgery = context.HttpContext.RequestServices.GetService(typeof(IAntiforgery)) as IAntiforgery;

        if (antiforgery == null)
            return;

        AntiforgeryTokenSet tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

        if (!context.HttpContext.Request.Form.ContainsKey(tokens.FormFieldName))
        {
            return;
        }

        var currentFormId = context.HttpContext.Request.Form[tokens.FormFieldName].ToString();
        var lastToken = "" + context.HttpContext.Session.GetString(UniqFormuId);

        if (lastToken.Equals(currentFormId))
        {
            context.ModelState.AddModelError(string.Empty, "Looks like you accidentally submitted the same form twice.");
            return;
        }

        context.HttpContext.Session.Remove(UniqFormuId);
        context.HttpContext.Session.SetString(UniqFormuId, currentFormId);
        await context.HttpContext.Session.CommitAsync();
    }
}

使用

[HttpPost]
[PreventDoublePost]
public ActionResult<TodoItem> Create([FromForm] TodoItem item)
{
    AllItems.Add(item);
    return CreatedAtRoute("GetTodo", new { id = item.Id }, item);
}

测试

# test post 
POST https://localhost:44352/TodoItem/ HTTP/1.1
content-type: application/x-www-form-urlencoded
cache-control: no-cache

__RequestVerificationToken=form_id_1
&id=86336
&Summary=ttt
###

既然PreventDoublePostAttribute属性只使用了tokens.FormFieldName,那么hardcode一个hidden formid应该也可以,这样就不需要AntiForgery了。
再说AntiForgery也不是这么用的。

参考