使用 $.ajax 发布 JSON 数据时如何提供 AntiForgeryToken?

Posted

技术标签:

【中文标题】使用 $.ajax 发布 JSON 数据时如何提供 AntiForgeryToken?【英文标题】:How can I supply an AntiForgeryToken when posting JSON data using $.ajax? 【发布时间】:2011-02-23 19:20:53 【问题描述】:

我正在使用如下代码:

首先,我将使用控制器操作的正确值填充一个数组变量。

使用下面的代码,我认为只需在 javascript 代码中添加以下行就应该非常简单:

data["__RequestVerificationToken"] = $('[name=__RequestVerificationToken]').val();

<%= html.AntiForgeryToken() %> 在正确的位置,动作有一个[ValidateAntiForgeryToken]

但我的控制器操作一直说:“无效的伪造令牌”

我在这里做错了什么?

代码

data["fiscalyear"] = fiscalyear;
data["subgeography"] = $(list).parent().find('input[name=subGeography]').val();
data["territories"] = new Array();

$(items).each(function() 
    data["territories"].push($(this).find('input[name=territory]').val());
);

    if (url != null) 
        $.ajax(
        
            dataType: 'JSON',
            contentType: 'application/json; charset=utf-8',
            url: url,
            type: 'POST',
            context: document.body,
            data: JSON.stringify(data),
            success: function()  refresh(); 
        );
    

【问题讨论】:

【参考方案1】:

自 MVC 4 以来,您不需要 ValidationHttpRequestWrapper 解决方案。据此link。

    将令牌放在标题中。 创建过滤器。 将属性添加到您的方法中。

这是我的解决方案:

var token = $('input[name="__RequestVerificationToken"]').val();
var headers = ;
headers['__RequestVerificationToken'] = token;
$.ajax(
    type: 'POST',
    url: '/MyTestMethod',
    contentType: 'application/json; charset=utf-8',
    headers: headers,
    data: JSON.stringify(
        Test: 'test'
    ),
    dataType: "json",
    success: function () ,
    error: function (xhr) 
);


[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class ValidateJsonAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter

    public void OnAuthorization(AuthorizationContext filterContext)
    
        if (filterContext == null)
        
            throw new ArgumentNullException("filterContext");
        

        var httpContext = filterContext.HttpContext;
        var cookie = httpContext.Request.Cookies[AntiForgeryConfig.CookieName];
        AntiForgery.Validate(cookie != null ? cookie.Value : null, httpContext.Request.Headers["__RequestVerificationToken"]);
    



[HttpPost]
[AllowAnonymous]
[ValidateJsonAntiForgeryToken]
public async Task<JsonResult> MyTestMethod(string Test)

    return Json(true);

【讨论】:

你把公开课ValidateJsonAntiForgeryTokenAttribute 放在哪里了? 我直接在项目根目录中创建了一个名为 Filters 的文件夹,我在其中创建了一个名为 ValidateJsonAntiForgeryTokenAttribute.cs 的类。 这对我仍然不起作用。我在项目根目录中的文件夹中创建了新的 .CS 文件,在我的 ActionResult 上有 [ValidateJsonAntiForgeryToken],然后完全按照你的方式拥有 JS。 Chrome 开发者工具“Network > PageName > Headers”显示:__RequestVerificationToken:egrd5Iun...8AH6_t8w2Request Headers 下。还有什么问题!? 我现在可以使用了;这很可能是我的缓存问题!谢谢!这个答案很棒! 优雅的解决方案,很好地使用了属性并导致代码更简洁。【参考方案2】:

问题在于,应该处理此请求并标有[ValidateAntiForgeryToken] 的控制器操作期望与请求一起发布一个名为__RequestVerificationToken 的参数。

由于您使用 JSON.stringify(data) 将您的表单转换为其 JSON 表示形式,因此没有发布此类参数,因此引发了异常。

所以我可以在这里看到两种可能的解决方案:

数字 1:使用 x-www-form-urlencoded 而不是 JSON 发送请求参数:

data["__RequestVerificationToken"] = $('[name=__RequestVerificationToken]').val();
data["fiscalyear"] = fiscalyear;
// ... other data if necessary

$.ajax(
    url: url,
    type: 'POST',
    context: document.body,
    data: data,
    success: function()  refresh(); 
);

数字2:将请求分成两个参数:

data["fiscalyear"] = fiscalyear;
// ... other data if necessary
var token = $('[name=__RequestVerificationToken]').val();

$.ajax(
    url: url,
    type: 'POST',
    context: document.body,
    data:  __RequestVerificationToken: token, jsonRequest: JSON.stringify(data) ,
    success: function()  refresh(); 
);

因此,在所有情况下,您都需要发布 __RequestVerificationToken 值。

【讨论】:

我喜欢这种方法并且它有效...只要您不期望通过 MVC 2 Futures/MVC 3 类 JsonValueProviderFactory 对字符串化 json 对象进行水合,并且您自己手动处理水合所以你知道忽略 __RequestVerificationToken。如果我不告诉 contentType 期望 $.ajax 的 json,则处理验证令牌,但 json 对象没有水合。如果我告诉 set json contentType 那么防伪验证失败。因此,我将研究 TWith2Sugars 解决方案。但以上确实有效! 如果您将参数传递给包含 ajax 调用的函数,您可以使用 $.extend 来“不显眼地”添加标记。 ` var data = $.extend(parameters, __RequestVerificationToken: token, jsonRequest: parameters ); `:***.com/questions/617036/appending-to-a-json-object @kdawg 你是如何让你的代码工作的?或者你如何定义data变量? @GregOgle 这只是在您不发布 JSON 的更简单的情况下,在这个答案中已经通过一个简单的分配很好地涵盖了这一点。不需要 $.extend() 的 CPU 滴答声。 我使用了@Darin Dimitrov 提出的“数字 2”,为了让它工作,我必须删除我为 $.ajax 设置的以下参数:dataType: 'JSON'contentType: 'application/json; charset=utf-8'完全就像达林·迪米特洛夫在他的帖子中所说的那样。【参考方案3】:

我只是在我当前的项目中实现这个实际问题。我对所有需要经过身份验证的用户的 Ajax POST 都这样做了。

首先,我决定挂钩我的 jQuery Ajax 调用,这样我就不会经常重复自己。这个 JavaScript sn-p 确保所有 ajax (post) 调用都会将我的请求验证令牌添加到请求中。注意:__RequestVerificationToken 这个名字是 .NET 框架使用的,所以我可以使用标准的 Anti-CSRF 功能,如下所示。

$(document).ready(function () 
    securityToken = $('[name=__RequestVerificationToken]').val();
    $('body').bind('ajaxSend', function (elm, xhr, s) 
        if (s.type == 'POST' && typeof securityToken != 'undefined') 
            if (s.data.length > 0) 
                s.data += "&__RequestVerificationToken=" + encodeURIComponent(securityToken);
            
            else 
                s.data = "__RequestVerificationToken=" + encodeURIComponent(securityToken);
            
        
    );
);

在您需要令牌可用于上述 JavaScript 代码的视图中,只需使用常见的 HTML-Helper。您基本上可以在任何地方添加此代码。我将它放在 if(Request.IsAuthenticated) 语句中:

@Html.AntiForgeryToken() // You can provide a string as salt when needed which needs to match the one on the controller

在您的控制器中只需使用标准的 ASP.NET MVC 反 CSRF 机制。我是这样做的(虽然我实际上使用了盐)。

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public JsonResult SomeMethod(string param)

    // Do something
    return Json(true);

使用 Firebug 或类似工具,您可以轻松查看您的 POST 请求现在如何附加 __RequestVerificationToken 参数。

【讨论】:

这适用于我的测试中 MVC 中大多数类型的 ajax 帖子,我已经放弃了布局并让它在没有更多代码的情况下工作。 在我看来你没有抓住重点。您的代码应仅与 application/x-www-form-urlencoded 内容类型的请求数据有效负载一起使用。 OP 想将他的请求数据有效负载发送为application/json。将 &amp;__Request... 附加到 JSON 有效负载应该会失败。 (他不是要求 JSON 响应,这就是您的代码示例,而是要求 JSON 请求。)【参考方案4】:

您可以设置$.ajax 的traditional 属性并将其设置为true,以将json 数据以url 编码形式发送。确保设置type:'POST'。使用这种方法,您甚至可以发送数组,而不必使用 JSON.stringyfy 或服务器端的任何更改(例如,创建自定义属性以嗅探标头)

我已经在 ASP.NET MVC3 和 jquery 1.7 设置上尝试过这个,它正在工作

下面是代码sn-p。

var data =  items: [1, 2, 3], someflag: true;

data.__RequestVerificationToken = $(':input[name="__RequestVerificationToken"]').val();

$.ajax(
    url: 'Test/FakeAction'
    type: 'POST',
    data: data
    dataType: 'json',
    traditional: true,
    success: function (data, status, jqxhr) 
        // some code after succes
    ,
    error: function () 
        // alert the error
    
);

这将与具有以下签名的 MVC 操作匹配

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public ActionResult FakeAction(int[] items, bool someflag)


【讨论】:

此方法在尝试发送包含 C# 值类型数组的数据时有效,但是在使用用户定义的类时我无法使其工作。你需要做些不同的事情来让它发挥作用吗?【参考方案5】:

我将令牌保存在我的 JSON 对象中,最后我修改了 ValidateAntiForgeryToken 类,以在帖子为 json 时检查 Request 对象的 InputStream。我已经写了一个blog post 关于它,希望你会发现它有用。

【讨论】:

【参考方案6】:

当您收到已发布的 JSON 时,您无需验证 AntiForgeryToken。

原因是AntiForgeryToken被创建来防止CSRF。由于您无法将 AJAX 数据发布到其他主机,并且 HTML 表单无法提交 JSON 作为请求正文,因此您不必保护您的应用免受发布的 JSON 的影响。

【讨论】:

这并不总是正确的。可以使用 HTML 表单伪造 JSON 帖子。如果您查看 AjaxRequestExtensions.IsAjaxRequest,它会在请求正文中检查“X-Requested-With”,而不仅仅是标头。因此,您要么自行验证以确保使用 AJAX 发布数据,要么添加 AntiForgeryToken。 但是请求的正文不会是 JSON,而是 form-url-encoded。 谁说你不能将数据 ajax 到另一个主机? ***.com/questions/298745/… 同意,但我的声明是为了回应“您不能将 Ajax 数据发布到另一台主机”。您可以发布数据。如果您的意思有所不同,也许手头有一个编辑,因为它看起来像您不能那样做。 如果您可以要求 Action 仅可用于 AJAX 请求,但您不能这样做,这将是正确的。就像声明的那样。您唯一能做的就是锁定 Action 以仅接受 application/json 作为请求的主体。但是我不太熟悉如何将 MVC 中的动作限制为特定的内容类型,我猜你必须做很多自定义工作。据我所知,这不是开箱即用的功能。【参考方案7】:

我已经用 RequestHeader 全局解决了。

$.ajaxPrefilter(function (options, originalOptions, jqXhr) 
    if (options.type.toUpperCase() === "POST") 
        // We need to add the verificationToken to all POSTs
        if (requestVerificationTokenVariable.length > 0)
            jqXhr.setRequestHeader("__RequestVerificationToken", requestVerificationTokenVariable);
    
);

其中 requestVerificationTokenVariable 是一个包含令牌值的变量字符串。 然后所有 ajax 调用将令牌发送到服务器,但默认 ValidateAntiForgeryTokenAttribute 获取 Request.Form 值。 我已经编写并添加了这个将令牌从标头复制到 request.form 的 globalFilter,然后我可以使用默认的 ValidateAntiForgeryTokenAttribute:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)

      filters.Add(new GlobalAntiForgeryTokenAttribute(false));



public class GlobalAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter

    private readonly bool autoValidateAllPost;

    public GlobalAntiForgeryTokenAttribute(bool autoValidateAllPost)
    
        this.autoValidateAllPost = autoValidateAllPost;
    

    private const string RequestVerificationTokenKey = "__RequestVerificationToken";
    public void OnAuthorization(AuthorizationContext filterContext)
    
        var req = filterContext.HttpContext.Request;
        if (req.HttpMethod.ToUpperInvariant() == "POST")
        
            //gestione per ValidateAntiForgeryToken che gestisce solo il recupero da Request.Form (non disponibile per le chiamate ajax json)
            if (req.Form[RequestVerificationTokenKey] == null && req.IsAjaxRequest())
            
                var token = req.Headers[RequestVerificationTokenKey];
                if (!string.IsNullOrEmpty(token))
                
                    req.Form.SetReadOnly(false);
                    req.Form[RequestVerificationTokenKey] = token;
                    req.Form.SetReadOnly(true);
                
            

            if (autoValidateAllPost)
                AntiForgery.Validate();
        
    


public static class NameValueCollectionExtensions

    private static readonly PropertyInfo NameObjectCollectionBaseIsReadOnly = typeof(NameObjectCollectionBase).GetProperty("IsReadOnly", BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance);

    public static void SetReadOnly(this NameValueCollection source, bool readOnly)
    
        NameObjectCollectionBaseIsReadOnly.SetValue(source, readOnly);
    

这对我有用:)

【讨论】:

这似乎是最好的选择。一个容易找到的过滤器(如果另一个开发人员来到这个项目),并允许不需要“jsonified”的普通请求在他们的请求中包含普通令牌(例如:对于已经存在但你不想要的项目更改代码中的每个 ajax 调用)。它使用额外的标题,并且可以通过“JQuery.ajaxSetup”轻松添加以自动包含在标题中。【参考方案8】:

您无法验证 contentType: 'application/json; 类型的内容charset=utf-8' 因为您的日期不会上传到请求的 Form 属性中,而是在 InputStream 属性中,并且您永远不会拥有此 Request.Form["__RequestVerificationToken"]。

这将始终为空,验证将失败。

【讨论】:

【参考方案9】:

查看Dixin's Blog 以获得关于如何做到这一点的精彩帖子。

另外,为什么不使用 $.post 而不是 $.ajax?

除了该页面上的 jQuery 插件,您还可以执行以下简单操作:

        data = $.appendAntiForgeryToken(data,null);

        $.post(url, data, function()  refresh(); , "json");

【讨论】:

【参考方案10】:

使用 Newtonsoft.JSON 库可以更轻松地使用 AntiForgerytoken 发布基于 AJAX 的模型 以下方法对我有用: 像这样保留您的 AJAX 帖子:

$.ajax(
  dataType: 'JSON',
  url: url,
  type: 'POST',
  context: document.body,
  data: 
    '__RequestVerificationToken': token,
    'model_json': JSON.stringify(data)
  ;,
  success: function() 
    refresh();
  
);

然后在您的 MVC 操作中:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(FormCollection data) 
 var model = JsonConvert.DeserializeObject < Order > (data["model_json"]);
 return Json(1);

希望这会有所帮助:)

【讨论】:

【参考方案11】:

在发布 JSON 时,我不得不有点阴暗地验证防伪令牌,但它确实有效。

//If it's not a GET, and the data they're sending is a string (since we already had a separate solution in place for form-encoded data), then add the verification token to the URL, if it's not already there.
$.ajaxSetup(
    beforeSend: function (xhr, options) 
        if (options.type && options.type.toLowerCase() !== 'get' && typeof (options.data) === 'string' && options.url.indexOf("?__RequestVerificationToken=") < 0 && options.url.indexOf("&__RequestVerificationToken=") < 0) 
            if (options.url.indexOf('?') < 0) 
                options.url += '?';
            
            else 
                options.url += '&';
            
            options.url += "__RequestVerificationToken=" + encodeURIComponent($('input[name=__RequestVerificationToken]').val());
        
    
);

但是,正如一些人已经提到的,验证只检查表单——不是 JSON,也不是查询字符串。所以,我们覆盖了属性的行为。重新实现所有的验证会很糟糕(而且可能不安全),所以我只是覆盖了 Form 属性,如果令牌在 QueryString 中传递,则内置验证认为它在 Form 中。

这有点棘手,因为表单是只读的,但可行。

    if (IsAuth(HttpContext.Current) && !IsGet(HttpContext.Current))
    
        //if the token is in the params but not the form, we sneak in our own HttpContext/HttpRequest
        if (HttpContext.Current.Request.Params != null && HttpContext.Current.Request.Form != null
            && HttpContext.Current.Request.Params["__RequestVerificationToken"] != null && HttpContext.Current.Request.Form["__RequestVerificationToken"] == null)
        
            AntiForgery.Validate(new ValidationHttpContextWrapper(HttpContext.Current), null);
        
        else
        
            AntiForgery.Validate(new HttpContextWrapper(HttpContext.Current), null);
        
    

    //don't validate un-authenticated requests; anyone could do it, anyway
    private static bool IsAuth(HttpContext context)
    
        return context.User != null && context.User.Identity != null && !string.IsNullOrEmpty(context.User.Identity.Name);
    

    //only validate posts because that's what CSRF is for
    private static bool IsGet(HttpContext context)
    
        return context.Request.HttpMethod.ToUpper() == "GET";
    

...

internal class ValidationHttpContextWrapper : HttpContextBase

    private HttpContext _context;
    private ValidationHttpRequestWrapper _request;

    public ValidationHttpContextWrapper(HttpContext context)
        : base()
    
        _context = context;
        _request = new ValidationHttpRequestWrapper(context.Request);
    

    public override HttpRequestBase Request  get  return _request;  

    public override IPrincipal User
    
        get  return _context.User; 
        set  _context.User = value; 
    


internal class ValidationHttpRequestWrapper : HttpRequestBase

    private HttpRequest _request;
    private System.Collections.Specialized.NameValueCollection _form;

    public ValidationHttpRequestWrapper(HttpRequest request)
        : base()
    
        _request = request;
        _form = new System.Collections.Specialized.NameValueCollection(request.Form);
        _form.Add("__RequestVerificationToken", request.Params["__RequestVerificationToken"]);
    

    public override System.Collections.Specialized.NameValueCollection Form  get  return _form;  

    public override string ApplicationPath  get  return _request.ApplicationPath;  
    public override HttpCookieCollection Cookies  get  return _request.Cookies;  

为了简洁起见,我们的解决方案还有其他一些不同之处(具体来说,我们使用的是 HttpModule,因此我们不必将属性添加到每个 POST)。有需要我可以加。

【讨论】:

【参考方案12】:

对我来说不幸的是,其他答案依赖于 jquery 处理的一些请求格式,并且在直接设置有效负载时它们都不起作用。 (公平地说,将它放在标题中会起作用,但我不想走那条路。)

要在beforeSend 函数中完成此操作,请执行以下操作。 $.params() 将对象转换为标准形式/url 编码格式。

我尝试了各种使用令牌对 json 进行字符串化的变体,但都没有奏效。

$.ajax(
...other params...,
beforeSend: function(jqXHR, settings)

    var token = ''; //get token

    data = 
        '__RequestVerificationToken' : token,
        'otherData': 'value'
     ; 
    settings.data = $.param(data);
    
);

```

【讨论】:

如果有错误,请告诉我——我只是手动输入,而不是复制和粘贴我真正的代码:\【参考方案13】:

您应该将 AntiForgeryToken 放在表单标签中:

@using (Html.BeginForm(actionName:"", controllerName:"",routeValues:null, method: FormMethod.Get, htmlAttributes: new  @class="form-validator" ))

    @Html.AntiForgeryToken();

然后在javascript中修改如下代码为

var DataToSend = [];
DataToSend.push(JSON.stringify(data), $('form.form-validator').serialize());
$.ajax(
  dataType: 'JSON',
  contentType: 'application/json; charset=utf-8',
  url: url,
  type: 'POST',
  context: document.body,
  data: DataToSend,
  success: function() 
    refresh();
  
);

那么您应该能够使用 ActionResult 注释来验证请求

[ValidateAntiForgeryToken]
        [HttpPost]

我希望这会有所帮助。

【讨论】:

如果您使用 json 发布它,它不必在表单标签中,除非您正在序列化表单。您的方法有效,但不必要地复杂。您可以将令牌附加到数据中。

以上是关于使用 $.ajax 发布 JSON 数据时如何提供 AntiForgeryToken?的主要内容,如果未能解决你的问题,请参考以下文章

js面试题,ajax请求时,如何解释json数据

ajax请求时如何解析json数据

Vue如何mock数据模拟Ajax请求

Flask提供json api跨域访问,ajax接收json数据

如何使用 Typescript 进行 AJAX 请求? (使用 JSON)

如何在线保存/加载数据(使用 AJAX 和 JSON 存储数据)和离线(本地)