【问题标题】:How can I supply an AntiForgeryToken when posting JSON data using $.ajax?使用 $.ajax 发布 JSON 数据时如何提供 AntiForgeryToken?
【发布时间】: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(); }
        });
    }

【问题讨论】:

    标签: asp.net-mvc ajax json antiforgerytoken


    【解决方案1】:

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

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

    这是我的解决方案:

    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 }); `:stackoverflow.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】:

    您可以设置$.ajaxtraditional 属性并将其设置为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 到另一个主机? stackoverflow.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 发布它,它不必在表单标签中,除非您正在序列化表单。您的方法有效,但不必要地复杂。您可以将令牌附加到数据中。
              猜你喜欢
              • 1970-01-01
              • 2015-08-19
              • 2021-08-28
              • 1970-01-01
              • 1970-01-01
              • 2014-04-08
              • 1970-01-01
              • 2013-01-25
              • 1970-01-01
              相关资源
              最近更新 更多