Wie kann ich ein AntiForgeryToken bereitstellen, wenn JSON-Daten mit $ .ajax veröffentlicht werden?


77

Ich verwende den folgenden Code in diesem Beitrag:

Zuerst fülle ich eine Array-Variable mit den richtigen Werten für die Controller-Aktion.

Mit dem folgenden Code sollte es meiner Meinung nach sehr einfach sein, dem JavaScript-Code nur die folgende Zeile hinzuzufügen:

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

Das <%= Html.AntiForgeryToken() %>ist am richtigen Ort, und die Aktion hat eine[ValidateAntiForgeryToken]

Aber meine Controller-Aktion sagt immer wieder: "Ungültiger Fälschungs-Token"

Was mache ich hier falsch?

Code

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(); }
        });
    }

Antworten:


67

Sie benötigen die ValidationHttpRequestWrapper-Lösung seit MVC 4 nicht mehr. Laut diesem Link .

  1. Setzen Sie den Token in die Überschriften.
  2. Erstellen Sie einen Filter.
  3. Fügen Sie das Attribut Ihrer Methode hinzu.

Hier ist meine Lösung:

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);
}

Wo hast du die öffentliche Klasse ValidateJsonAntiForgeryTokenAttribute hingelegt?
Turp

1
Ich habe einen Ordner direkt im Projektstamm mit dem Namen Filter erstellt, in dem ich eine Klasse mit dem Namen ValidateJsonAntiForgeryTokenAttribute.cs erstellt habe.
Ken Q

Das hat bei mir immer noch nicht funktioniert. Ich habe die neue CS-Datei in einem Ordner im Stammverzeichnis meines Projekts erstellt, habe die [ValidateJsonAntiForgeryToken]auf meiner ActionResultund habe dann die JS genau so, wie Sie sie haben. Chrome Developer Tools "Netzwerk> Seitenname> Header" zeigt: __RequestVerificationToken:egrd5Iun...8AH6_t8w2unter Request Headers. Was könnte sonst noch falsch sein?
Turp

1
Ich habe es jetzt funktioniert; Es war mehr als wahrscheinlich ein Caching-Problem für mich! Vielen Dank! Diese Antwort war großartig!
Turp

2
Elegante Lösung, sehr gute Verwendung von Attributen und führt zu saubererem Code.
Aerokneeus

49

Was falsch ist, ist, dass die Controller-Aktion, die diese Anforderung verarbeiten soll und die mit gekennzeichnet ist, [ValidateAntiForgeryToken]einen Parameter erwartet, der aufgerufen wird __RequestVerificationToken, zusammen mit der Anforderung POSTED zu werden.

Es gibt keinen von Ihnen verwendeten POST-Parameter, JSON.stringify(data)der Ihr Formular in seine JSON-Darstellung konvertiert, und daher wird die Ausnahme ausgelöst.

Daher kann ich hier zwei mögliche Lösungen sehen:

Nummer 1: Verwenden Sie x-www-form-urlencodedstatt JSONzum Senden Ihrer Anforderungsparameter:

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(); }
});

Nummer 2: Trennen Sie die Anfrage in zwei Parameter:

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(); }
});

In allen Fällen müssen Sie den __RequestVerificationTokenWert POSTEN .


4
Ich mag diesen Ansatz und er funktioniert ... solange Sie nicht erwarten, dass das stringifizierte json-Objekt über die JsonValueProviderFactory der MVC 2 Futures / MVC 3-Klasse hydratisiert wird, und Sie die Hydratation selbst manuell durchführen, damit Sie das __RequestVerificationToken ignorieren können. Wenn ich dem contentType nicht sage, dass er json für $ .ajax erwarten soll, wird das Überprüfungstoken behandelt, aber das json-Objekt wird nicht hydratisiert. Wenn ich set json contentType mitteile, schlägt die Anti-Forgery-Validierung fehl. Aus diesem Grund werde ich mir die TWith2Sugars-Lösung ansehen. Aber das oben genannte funktioniert tatsächlich!
Kdawg

Wenn Sie die Parameter an die Funktion übergeben haben, die den Ajax-Aufruf enthält, können Sie mit $ .extend das Token 'unauffällig' hinzufügen. `var data = $ .extend (Parameter, {__RequestVerificationToken: Token, jsonRequest: Parameter}); `: stackoverflow.com/questions/617036/appending-to-a-json-object
Greg Ogle

@kdawg Wie hast du deinen Code genau zum Laufen gebracht? Oder wie definieren Sie eine dataVariable?
Hardywang

@GregOgle Dies ist nur in dem einfacheren Fall der Fall, in dem Sie kein JSON veröffentlichen, das in dieser Antwort bereits mit einer einfachen Zuweisung ausführlich behandelt wird. Die CPU-Ticks von $ .extend () sind nicht erforderlich.
Chris Moschini

Ich habe „Nummer 2“ Vorschlag @Darin Dimitrov verwendet, und um für sie zu arbeiten ich die folgenden Parameter entfernen , musste ich für $ .ajax hatte: dataType: 'JSON'und auch contentType: 'application/json; charset=utf-8', genau wie Darin Dimitrov es in seinem Beitrag hat.
Sasha

10

Ich habe gerade dieses eigentliche Problem in meinem aktuellen Projekt implementiert. Ich habe es für alle Ajax-POSTs gemacht, die einen authentifizierten Benutzer benötigen.

Zunächst entschied ich mich, meine jQuery Ajax-Anrufe zu verknüpfen, damit ich mich nicht zu oft wiederhole. Dieses JavaScript-Snippet stellt sicher, dass alle Ajax- (Post-) Aufrufe mein Anforderungsvalidierungstoken zur Anforderung hinzufügen. Hinweis: Der Name __RequestVerificationToken wird vom .NET Framework verwendet, sodass ich die unten gezeigten Standard-Anti-CSRF-Funktionen verwenden kann.

$(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);
            }
        }
    });
});

Verwenden Sie in Ihren Ansichten, in denen das Token für den obigen JavaScript-Code verfügbar sein muss, einfach den allgemeinen HTML-Helper. Sie können diesen Code grundsätzlich hinzufügen, wo immer Sie möchten. Ich habe es in eine if-Anweisung (Request.IsAuthenticated) eingefügt:

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

Verwenden Sie in Ihrem Controller einfach den Standard-Anti-CSRF-Mechanismus von ASP.NET MVC. Ich habe es so gemacht (obwohl ich tatsächlich ein Salz verwendet habe).

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public JsonResult SomeMethod(string param)
{
    // Do something
    return Json(true);
}

Mit Firebug oder einem ähnlichen Tool können Sie leicht sehen, wie an Ihre POST-Anforderungen jetzt ein __RequestVerificationToken-Parameter angehängt ist.


Dies funktioniert mit den meisten Arten von Ajax-Posts in der MVC aus meinen Tests. Ich habe das Layout gelöscht und es ohne weiteren Code funktionieren lassen.

3
Mir scheint, Sie haben den Punkt verfehlt. Ihr Code sollte nur mit einer Anforderungsdaten-Nutzlast als application/x-www-form-urlencodedInhaltstyp funktionieren . Das OP wollte seine Anforderungsdaten-Nutzdaten als senden application/json. Das Anhängen &__Request...an eine JSON-Nutzlast sollte fehlschlagen. (Er bat nicht um eine JSON-Antwort, was Ihr Codebeispiel ist, sondern um eine JSON-Anfrage.)
Frédéric

7

Sie können einstellen , $ Schnipsel ‚s traditionalAttribut und setzen Sie ihn auf true, zu json Daten als URL kodierter Form zu senden. Stellen Sie sicher, dass Sie einstellen type:'POST'. Mit dieser Methode können Sie sogar Arrays senden und müssen weder JSON.stringyfy noch Änderungen auf der Serverseite verwenden (z. B. benutzerdefinierte Attribute für den Sniff-Header erstellen).

Ich habe dies auf ASP.NET MVC3 und jquery 1.7 Setup versucht und es funktioniert

Es folgt das Code-Snippet.

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
    }
});

Dies stimmt mit der MVC-Aktion mit der folgenden Signatur überein

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

Diese Methode funktioniert, wenn versucht wird, Daten zu senden, die Arrays von C # -Werttypen enthalten. Bei Verwendung benutzerdefinierter Klassen kann sie jedoch nicht zum Laufen gebracht werden. Müssen Sie etwas anders machen, damit das funktioniert?
Liam Flanagan

6

Sie können einen Inhalt vom Typ contentType nicht überprüfen: 'application / json; charset = utf-8 ', da Ihr Datum nicht in der Form- Eigenschaft der Anforderung, sondern in der InputStream-Eigenschaft hochgeladen wird und Sie diese Request.Form ["__ RequestVerificationToken"] niemals haben.

Dies ist immer leer und die Validierung schlägt fehl.


5

Ich halte das Token in meinem JSON-Objekt und habe am Ende die ValidateAntiForgeryToken-Klasse geändert, um den InputStream des Request- Objekts zu überprüfen, wenn der Beitrag json ist. Ich habe einen Blog-Beitrag darüber geschrieben, hoffentlich finden Sie ihn nützlich.


5

Sie müssen niemals ein AntiForgeryToken validieren, wenn Sie JSON erhalten.

Der Grund dafür ist, dass AntiForgeryToken erstellt wurde, um CSRF zu verhindern. Da Sie AJAX-Daten nicht auf einem anderen Host veröffentlichen können und HTML-Formulare JSON nicht als Anforderungshauptteil senden können, müssen Sie Ihre App nicht vor veröffentlichtem JSON schützen.


3
Das ist nicht immer richtig. Es ist möglich, einen JSON-Beitrag mithilfe von HTML-Formularen zu fälschen. Wenn Sie sich AjaxRequestExtensions.IsAjaxRequest ansehen, wird im Anfragetext nach "X-Requested-With" gesucht, nicht nur nach den Headern. Sie führen entweder Ihre eigene Validierung durch, um sicherzustellen, dass die Daten mit AJAX veröffentlicht werden, oder Sie fügen AntiForgeryToken hinzu.
Jeow Li Huan

Der Hauptteil der Anfrage ist jedoch nicht JSON, sondern form-url-codiert.
Antoine Leclair

3
Wer hat gesagt, dass Sie keine Daten an einen anderen Host übertragen können? stackoverflow.com/questions/298745/…
Adam Tuliper - MSFT

1
Einverstanden, aber meine Aussage war eine Antwort auf "Sie können Ajax-Daten nicht auf einem anderen Host veröffentlichen". Sie können Daten posten. Wenn Sie etwas anderes gemeint haben Vielleicht ist eine Bearbeitung zur Hand, da sie lautet, als könnten Sie das nicht tun.
Adam Tuliper - MSFT

2
Dies wäre der Fall, wenn Sie eine Aktion benötigen könnten, die nur für AJAX-Anforderungen verfügbar ist, dies jedoch nicht. Wie erwähnt. Das einzige, was Sie tun können, ist, die Aktion zu sperren, um nur application / json als Hauptteil der Anforderung zu akzeptieren. Aber ich bin nicht wirklich vertraut damit, wie Sie eine Aktion in MVC nur auf einen bestimmten Inhaltstyp beschränken. Ich vermute, Sie müssten eine Menge benutzerdefinierter Arbeit leisten. Soweit ich weiß, handelt es sich nicht um eine Out-of-the-Box-Funktion.
Nicholi

3

Ich habe es global mit RequestHeader gelöst.

$.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);
    }
});

Dabei ist requestVerificationTokenVariable eine variable Zeichenfolge, die den Tokenwert enthält. Dann senden alle Ajax-Aufrufe das Token an den Server, aber das Standard-ValidateAntiForgeryTokenAttribute erhält den Wert Request.Form. Ich habe diesen globalFilter geschrieben und hinzugefügt, der das Token vom Header in request.form kopiert, als ich das Standard-ValidateAntiForgeryTokenAttribute verwenden kann:

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);
    }
}

Diese Arbeit für mich :)


2

In Dixins Blog finden Sie einen großartigen Beitrag dazu.

Warum nicht auch $ .post anstelle von $ .ajax verwenden?

Zusammen mit dem jQuery-Plugin auf dieser Seite können Sie dann Folgendes tun:

        data = $.appendAntiForgeryToken(data,null);

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

2

AJAX-basiertes Modellposting mit AntiForgerytoken kann mit der Newtonsoft.JSON-Bibliothek etwas vereinfacht werden. Der folgende
Ansatz hat für mich funktioniert:
Behalten Sie Ihren AJAX-Post wie folgt bei :

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

Dann in Ihrer MVC-Aktion:

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

Hoffe das hilft :)


1

Ich musste ein wenig zwielichtig sein, um Anti-Fälschungs-Token beim Posten von JSON zu validieren, aber es funktionierte.

//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());
        }
    }
});

Wie bereits erwähnt, überprüft die Validierung jedoch nur das Formular - nicht JSON und nicht die Abfragezeichenfolge. Also haben wir das Verhalten des Attributs überschrieben. Die erneute Implementierung der gesamten Validierung wäre schrecklich (und wahrscheinlich nicht sicher) gewesen. Daher habe ich die Form-Eigenschaft einfach überschrieben, um, wenn das Token im QueryString übergeben wurde, die integrierte Validierung THINK zu erhalten, die im Formular enthalten war.

Das ist etwas schwierig, da das Formular schreibgeschützt, aber machbar ist.

    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; } }
}

Es gibt noch einige andere Dinge, die an unserer Lösung anders sind (insbesondere verwenden wir ein HttpModule, damit wir nicht jedem einzelnen POST das Attribut hinzufügen müssen), die ich zugunsten der Kürze ausgelassen habe. Ich kann es bei Bedarf hinzufügen.


0

Leider basieren die anderen Antworten auf einer von jquery verarbeiteten Anforderungsformatierung, und keine davon funktionierte beim direkten Einstellen der Nutzdaten. (Um fair zu sein, hätte es funktioniert, es in den Header zu setzen, aber ich wollte diesen Weg nicht gehen.)

Um dies in der beforeSendFunktion zu erreichen, funktioniert Folgendes. $.params()transformiert das Objekt in das Standardformular / URL-codierte Format.

Ich hatte alle möglichen Variationen von Stringing Json mit dem Token ausprobiert und keine davon funktionierte.

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

    var token = ''; //get token

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

`` `


Bitte lassen Sie mich wissen, wenn es einen Fehler gibt - ich habe ihn nur von Hand geschrieben, anstatt den durcheinandergebrachten
Haufen

-1

Sie sollten AntiForgeryToken in ein Formular-Tag einfügen:

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

Ändern Sie dann in Javascript den folgenden Code

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();
  }
});

Dann sollten Sie in der Lage sein, die Anforderung mithilfe von ActionResult-Annotationen zu validieren

[ValidateAntiForgeryToken]
        [HttpPost]

Ich hoffe das hilft.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.