使用AspNet Core MVC在Ajax请求中验证AntiForgeryToken

时间:2016-04-14 16:20:41

标签: c# ajax asp.net-core-mvc dnx antiforgerytoken

我一直在尝试重新创建ValidateAntiForgeryToken的Ajax版本 - 有许多关于如何为以前版本的MVC执行此操作的博客文章,但是对于最新的MVC 6,没有任何代码是相关的。不过,我要追求的核心原则是对__RequestVerificationToken的Cookie和Header进行验证,而不是将Cookie与表单值进行比较。我使用的是MVC 6.0.0-rc1-final,dnx451框架,所有的Microsoft.Extensions库都是1.0.0-rc1-final。

我最初的想法是继承ValidateAntiForgeryTokenAttribute,但是看一下源代码,我需要返回自己的授权过滤器实现,让它看看标题。

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateAjaxAntiForgeryTokenAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
    public int Order { get; set; }
    public bool IsReusable => true;
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return serviceProvider.GetRequiredService<ValidateAjaxAntiforgeryTokenAuthorizationFilter>();
    }
}

因此,我制作了自己的ValidateAntiforgeryTokenAuthorizationFilter

版本
public class ValidateAjaxAntiforgeryTokenAuthorizationFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy
{
    private readonly IAntiforgery _antiforgery;
    private readonly ILogger _logger;
    public ValidateAjaxAntiforgeryTokenAuthorizationFilter(IAntiforgery antiforgery, ILoggerFactory loggerFactory)
    {
        if (antiforgery == null)
        {
            throw new ArgumentNullException(nameof(antiforgery));
        }
        _antiforgery = antiforgery;
        _logger = loggerFactory.CreateLogger<ValidateAjaxAntiforgeryTokenAuthorizationFilter>();
    }
    public async Task OnAuthorizationAsync(AuthorizationContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
        if (IsClosestAntiforgeryPolicy(context.Filters) && ShouldValidate(context))
        {
            try
            {
                await _antiforgery.ValidateRequestAsync(context.HttpContext);
            }
            catch (AjaxAntiforgeryValidationException exception)
            {
                _logger.LogInformation(1, string.Concat("Ajax Antiforgery token validation failed. ", exception.Message));
                context.Result = new BadRequestResult();
            }
        }
    }
    protected virtual bool ShouldValidate(AuthorizationContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
        return true;
    }
    private bool IsClosestAntiforgeryPolicy(IList<IFilterMetadata> filters)
    {
        // Determine if this instance is the 'effective' antiforgery policy.
        for (var i = filters.Count - 1; i >= 0; i--)
        {
            var filter = filters[i];
            if (filter is IAntiforgeryPolicy)
            {
                return object.ReferenceEquals(this, filter);
            }
        }
        Debug.Fail("The current instance should be in the list of filters.");
        return false;
    }
}

但是,我找不到包含IAntiforgeryPolicy的正确Nuget包和命名空间。虽然我在GitHub上找到了界面 - 我在哪个包中找到它?

我的下一次尝试是继续注射IAntiforgery,并将DefaultAntiforgery替换为我自己的AjaxAntiforgery

public class AjaxAntiforgery : DefaultAntiforgery
{
    private readonly AntiforgeryOptions _options;
    private readonly IAntiforgeryTokenGenerator _tokenGenerator;
    private readonly IAntiforgeryTokenSerializer _tokenSerializer;
    private readonly IAntiforgeryTokenStore _tokenStore;
    private readonly ILogger<AjaxAntiforgery> _logger;
    public AjaxAntiforgery(
        IOptions<AntiforgeryOptions> antiforgeryOptionsAccessor,
        IAntiforgeryTokenGenerator tokenGenerator,
        IAntiforgeryTokenSerializer tokenSerializer,
        IAntiforgeryTokenStore tokenStore,
        ILoggerFactory loggerFactory)
    {
        _options = antiforgeryOptionsAccessor.Value;
        _tokenGenerator = tokenGenerator;
        _tokenSerializer = tokenSerializer;
        _tokenStore = tokenStore;
        _logger = loggerFactory.CreateLogger<AjaxAntiforgery>();
    }
}

在我退出之前,我已经走了很远,因为ILoggerFactory CreateLogger<T>()上没有通用方法。 DefaultAntiforgery的源代码有Microsoft.Extensions.Options,但我无法在任何Nuget包中找到该命名空间。 Microsoft.Extensions.OptionsModel存在,但只会引入IOptions<out TOptions>界面。

要完成所有这些操作,一旦我让授权过滤器工作,或者我得到IAntiforgery的新实现,我在哪里或如何使用依赖注入来注册它以使用它 - 并且只有对于我将接受Ajax请求的操作?

3 个答案:

答案 0 :(得分:11)

我有类似的问题。我不知道在.NET中是否会发生任何变化,但当时我在 Startup.cs 中添加了以下行到 ConfigureServices 方法, line services.AddMvc(),以验证通过Ajax发送的AntiForgeryToken:

services.AddAntiforgery(options =>
{
    options.CookieName = "yourChosenCookieName"; 
    options.HeaderName = "RequestVerificationToken";
});

AJAX调用类似于以下内容:

var token = $('input[type=hidden][name=__RequestVerificationToken]', document).val();

var request = $.ajax({
    data: { 'yourField': 'yourValue' },
    ...
    headers: { 'RequestVerificationToken': token }
});   

然后,只需在您的操作中使用原生属性 [ValidadeAntiForgeryToken]

答案 1 :(得分:3)

我一直在与类似的情况搏斗,将角度POST与MVC6接口,并提出以下建议。

需要解决两个问题:将安全令牌放入MVC的防伪验证子系统,并将角度的JSON格式的回发数据转换为MVC模型。

我通过在Startup.Configure()中插入的一些自定义中间件来处理第一步。中间件类非常简单:

public static class UseAngularXSRFExtension
{
    public const string XSRFFieldName = "X-XSRF-TOKEN";

    public static IApplicationBuilder UseAngularXSRF( this IApplicationBuilder builder )
    {
        return builder.Use( next => context =>
        {
            switch( context.Request.Method.ToLower() )
            {
                case "post":
                case "put":
                case "delete":
                    if( context.Request.Headers.ContainsKey( XSRFFieldName ) )
                    {
                        var formFields = new Dictionary<string, StringValues>()
                        {
                            { XSRFFieldName, context.Request.Headers[XSRFFieldName] }
                        };

                        // this assumes that any POST, PUT or DELETE having a header
                        // which includes XSRFFieldName is coming from angular, so 
                        // overwriting context.Request.Form is okay (since it's not
                        // being parsed by MVC's internals anyway)
                        context.Request.Form = new FormCollection( formFields );
                    }

                    break;
            }

            return next( context );
        } );
    }
}

使用Startup.Configure()方法中的以下行将其插入管道:

app.UseAngularXSRF();

我在调用app.UseMVC()之前就已经这样做了。

请注意,此扩展名在任何POST,PUT或DELETE位置传输XSRF标头,并通过覆盖现有的表单字段集合来实现。这符合我的设计模式 - XSRF标头在请求中的唯一时间是它来自我编写的某些角度代码 - 但它可能不适合你的。

我还认为您需要配置防伪子系统以使用XSRF字段名称的正确名称(我不确定默认值是什么)。您可以通过将以下行插入Startup.ConfigureServices():

来完成此操作
    services.ConfigureAntiforgery( options => options.FormFieldName = UseAngularXSRFExtension.XSRFFieldName );

我在行service.AddAntiforgery()。

之前插入了这个

有几种方法可以将XSRF令牌放入请求流中。我所做的是在视图中添加以下内容:

...top of view...
@inject Microsoft.AspNet.Antiforgery.IAntiforgery af
...rest of view...

...inside the angular function...
            var postHeaders = {
                'X-XSRF-TOKEN': '@(af.GetTokens(this.Context).FormToken)',
                'Content-Type': 'application/json; charset=utf-8',
            };

            $http.post( '/Dataset/DeleteDataset', JSON.stringify({ 'siteID': siteID }),
                {
                    headers: postHeaders,
                })
...rest of view...

第二部分 - 翻译JSON数据 - 通过使用[FromBody]在您的操作方法上修饰模型类来处理:

        // the [FromBody] attribute on the model -- and a class model, rather than a
        // single integer model -- are necessary so that MVC can parse the JSON-formatted
        // text POSTed by angular
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult DeleteDataset( [FromBody] DeleteSiteViewModel model )
        {
}

[FromBody]仅适用于类实例。即使在我的情况下,我感兴趣的只是一个整数,我仍然需要假设一个类,它只包含一个整数属性。

希望这有帮助。

答案 2 :(得分:0)

在Ajax调用中使用反伪造令牌是可能的,但如果你想要确保Api,我真的建议使用访问令牌。

如果您依赖存储在Cookie中的身份令牌作为Api的身份验证,则需要编写代码以补偿Cookie身份验证超时,并且您的Ajax帖子被重定向到登录屏幕。这对于SPA和Angular应用程序尤其重要。

使用访问令牌实现,将允许您刷新访问令牌(使用刷新令牌),长时间运行会话并阻止cookie窃贼访问您的Apis ..它也将停止XSRF:)< / p>

访问令牌的目的是保护资源,例如Web Apis。