JQuery Ajax вызывает и Html.AntiForgeryToken () - PullRequest
195 голосов
/ 02 ноября 2010

Я реализовал в своем приложении защиту от CSRF-атак , следуя информации, которую я прочитал в некоторых постах в блогах в Интернете. В частности эти посты были драйвером моей реализации

В основном в этих статьях и рекомендациях говорится, что для предотвращения атаки CSRF любой должен реализовать следующий код:

1) Добавьте [ValidateAntiForgeryToken] к каждому действию, которое принимает глагол POST Http

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SomeAction( SomeModel model ) {
}

2) Добавьте вспомогательный <%= Html.AntiForgeryToken() %> внутри форм, который отправляет данные на сервер

<div style="text-align:right; padding: 8px;">
    <%= Html.AntiForgeryToken() %>
    <input type="submit" id="btnSave" value="Save" />
</div>

Во всяком случае, в некоторых частях моего приложения я выполняю POST Ajax с jQuery на сервер, не имея никакой формы. Это происходит, например, когда я позволяю пользователю нажимать на изображение, чтобы выполнить определенное действие.

Предположим, у меня есть таблица со списком действий. У меня есть изображение в столбце таблицы с надписью «Пометить активность как выполненную», и когда пользователь нажимает на эту операцию, я выполняю Ajax POST, как в следующем примере:

$("a.markAsDone").click(function (event) {
    event.preventDefault();
    $.ajax({
        type: "post",
        dataType: "html",
        url: $(this).attr("rel"),
        data: {},
        success: function (response) {
            // ....
        }
    });
});

Как я могу использовать <%= Html.AntiForgeryToken() %> в этих случаях? Должен ли я включить вспомогательный вызов в параметр data вызова Ajax?

Извините за длинный пост и большое спасибо за помощь

EDIT

Согласно jayrdub ответ, который я использовал следующим образом

$("a.markAsDone").click(function (event) {
    event.preventDefault();
    $.ajax({
        type: "post",
        dataType: "html",
        url: $(this).attr("rel"),
        data: {
            AddAntiForgeryToken({}),
            id: parseInt($(this).attr("title"))
        },
        success: function (response) {
            // ....
        }
    });
});

Ответы [ 19 ]

239 голосов
/ 02 ноября 2010

Я использую простую функцию js, как это

AddAntiForgeryToken = function(data) {
    data.__RequestVerificationToken = $('#__AjaxAntiForgeryForm input[name=__RequestVerificationToken]').val();
    return data;
};

Поскольку каждая форма на странице будет иметь одинаковое значение для токена, просто поместите что-то вроде этого на свою самую верхнюю главную страницу

<%-- used for ajax in AddAntiForgeryToken() --%>
<form id="__AjaxAntiForgeryForm" action="#" method="post"><%= Html.AntiForgeryToken()%></form>  

Затем в вашем вызове ajax do (отредактированный в соответствии с вашим вторым примером)

$.ajax({
    type: "post",
    dataType: "html",
    url: $(this).attr("rel"),
    data: AddAntiForgeryToken({ id: parseInt($(this).attr("title")) }),
    success: function (response) {
        // ....
    }
});
29 голосов
/ 25 августа 2012

Мне нравится решение, предоставляемое 360Airwalk, но оно может быть немного улучшено.

Первая проблема заключается в том, что если вы делаете $.post() с пустыми данными, jQuery не добавляет заголовок Content-Type, и в этом случае ASP.NET MVC не может получить и проверить токен. Таким образом, вы должны убедиться, что заголовок всегда там.

Еще одним улучшением является поддержка всех HTTP-глаголов с содержимым : POST, PUT, DELETE и т. Д. Хотя вы можете использовать только POST в своем приложении, лучше иметь общее решение и убедиться, что все данные, которые вы Получить с любым глаголом есть жетон против подделки.

$(document).ready(function () {
    var securityToken = $('[name=__RequestVerificationToken]').val();
    $(document).ajaxSend(function (event, request, opt) {
        if (opt.hasContent && securityToken) {   // handle all verbs with content
            var tokenParam = "__RequestVerificationToken=" + encodeURIComponent(securityToken);
            opt.data = opt.data ? [opt.data, tokenParam].join("&") : tokenParam;
            // ensure Content-Type header is present!
            if (opt.contentType !== false || event.contentType) {
                request.setRequestHeader( "Content-Type", opt.contentType);
            }
        }
    });
});
21 голосов
/ 18 января 2014

Я знаю, что есть много других ответов, но эта статья хороша и лаконична и заставляет вас проверять все ваши HttpPosts, а не только некоторые из них:

http://richiban.wordpress.com/2013/02/06/validating-net-mvc-4-anti-forgery-tokens-in-ajax-requests/

Он использует заголовки HTTP вместо попытки изменить коллекцию форм.

Сервер

//make sure to add this to your global action filters
[AttributeUsage(AttributeTargets.Class)]
public class ValidateAntiForgeryTokenOnAllPosts : AuthorizeAttribute
{
    public override void OnAuthorization( AuthorizationContext filterContext )
    {
        var request = filterContext.HttpContext.Request;

        //  Only validate POSTs
        if (request.HttpMethod == WebRequestMethods.Http.Post)
        {
            //  Ajax POSTs and normal form posts have to be treated differently when it comes
            //  to validating the AntiForgeryToken
            if (request.IsAjaxRequest())
            {
                var antiForgeryCookie = request.Cookies[AntiForgeryConfig.CookieName];

                var cookieValue = antiForgeryCookie != null
                    ? antiForgeryCookie.Value 
                    : null;

                AntiForgery.Validate(cookieValue, request.Headers["__RequestVerificationToken"]);
            }
            else
            {
                new ValidateAntiForgeryTokenAttribute()
                    .OnAuthorization(filterContext);
            }
        }
    }
}

Клиент

var token = $('[name=__RequestVerificationToken]').val();
var headers = {};
headers["__RequestVerificationToken"] = token;

$.ajax({
    type: 'POST',
    url: '/Home/Ajax',
    cache: false,
    headers: headers,
    contentType: 'application/json; charset=utf-8',
    data: { title: "This is my title", contents: "These are my contents" },
    success: function () {
        ...
    },
    error: function () {
        ...
    }
});
19 голосов
/ 06 марта 2013

Не использовать Html.AntiForgeryToken .Вместо этого используйте AntiForgery.GetTokens и AntiForgery.Validate из Web API, как описано в Предотвращение атак подделки межсайтовых запросов (CSRF) в приложении ASP.NET MVC .

17 голосов
/ 16 декабря 2014

Я чувствую себя здесь как продвинутый некромант, но через 4 года в MVC5 это все еще остается проблемой.

Для правильной обработки ajax-запросов токен защиты от подделки должен передаваться на сервер при ajax-вызовах. Интеграция его в ваши почтовые данные и модели является грязной и ненужной. Добавление токена в качестве пользовательского заголовка является чистым и пригодным для повторного использования - и вы можете настроить его так, чтобы вам не приходилось делать это каждый раз.

Существует исключение - ненавязчивый ajax не требует специальной обработки для вызовов ajax. Маркер передается как обычно в обычном скрытом поле ввода. Точно так же, как обычный POST.

_Layout.cshtml

В _layout.cshtml у меня есть этот блок JavaScript. Он не записывает токен в DOM, а использует jQuery для извлечения его из скрытого входного литерала, который генерирует помощник MVC. Волшебная строка, которая является именем заголовка, определяется как константа в классе атрибутов.

<script type="text/javascript">
    $(document).ready(function () {
        var isAbsoluteURI = new RegExp('^(?:[a-z]+:)?//', 'i');
        //http://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative

        $.ajaxSetup({
            beforeSend: function (xhr) {
                if (!isAbsoluteURI.test(this.url)) {
                    //only add header to relative URLs
                    xhr.setRequestHeader(
                       '@.ValidateAntiForgeryTokenOnAllPosts.HTTP_HEADER_NAME', 
                       $('@Html.AntiForgeryToken()').val()
                    );
                }
            }
        });
    });
</script>

Обратите внимание на использование одинарных кавычек в функции beforeSend - в отображаемом элементе ввода используются двойные кавычки, которые разбивают литерал JavaScript.

Клиентский JavaScript

Когда это выполняется, вызывается вышеуказанная функция beforeSend, и AntiForgeryToken автоматически добавляется в заголовки запроса.

$.ajax({
  type: "POST",
  url: "CSRFProtectedMethod",
  dataType: "json",
  contentType: "application/json; charset=utf-8",
  success: function (data) {
    //victory
  }
});

Серверная библиотека

Для обработки нестандартного токена требуется пользовательский атрибут. Это основано на решении @ viggity, но обрабатывает ненавязчивый Ajax правильно. Этот код можно спрятать в вашей общей библиотеке

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ValidateAntiForgeryTokenOnAllPosts : AuthorizeAttribute
{
    public const string HTTP_HEADER_NAME = "x-RequestVerificationToken";

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var request = filterContext.HttpContext.Request;

        //  Only validate POSTs
        if (request.HttpMethod == WebRequestMethods.Http.Post)
        {

            var headerTokenValue = request.Headers[HTTP_HEADER_NAME];

            // Ajax POSTs using jquery have a header set that defines the token.
            // However using unobtrusive ajax the token is still submitted normally in the form.
            // if the header is present then use it, else fall back to processing the form like normal
            if (headerTokenValue != null)
            {
                var antiForgeryCookie = request.Cookies[AntiForgeryConfig.CookieName];

                var cookieValue = antiForgeryCookie != null
                    ? antiForgeryCookie.Value
                    : null;

                AntiForgery.Validate(cookieValue, headerTokenValue);
            }
            else
            {
                new ValidateAntiForgeryTokenAttribute()
                    .OnAuthorization(filterContext);
            }
        }
    }
}

Сервер / Контроллер

Теперь вы просто применяете атрибут к своему действию. Более того, вы можете применить атрибут к контроллеру, и все запросы будут проверены.

[HttpPost]
[ValidateAntiForgeryTokenOnAllPosts]
public virtual ActionResult CSRFProtectedMethod()
{
  return Json(true, JsonRequestBehavior.DenyGet);
}
16 голосов
/ 04 апреля 2012

Я только что реализовал эту актуальную проблему в моем текущем проекте.Я сделал это для всех ajax-POST, для которых требовался аутентифицированный пользователь.

Прежде всего, я решил перехватить свои jjery ajax-вызовы, чтобы не повторяться слишком часто.этот фрагмент javascript гарантирует, что все вызовы ajax (post) добавят мой токен подтверждения запроса в запрос.Примечание: имя __RequestVerificationToken используется платформой .Net, поэтому я могу использовать стандартные функции Anti-CSRF, как показано ниже.

$(document).ready(function () {
    var 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-помощник.Вы можете добавить этот код везде, где хотите.Я поместил его в оператор 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 Anti-CSRF.Я сделал это так (хотя на самом деле я использовал Salt).

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

С помощью Firebug или аналогичного инструмента вы можете легко увидеть, как теперь к вашим запросам POST добавлен параметр __RequestVerificationToken.

11 голосов
/ 02 ноября 2010

Я думаю, все, что вам нужно сделать, это убедиться, что вход "__RequestVerificationToken" включен в запрос POST.Другая половина информации (то есть токен в файле cookie пользователя) уже автоматически отправляется с запросом AJAX POST.

Например,

$("a.markAsDone").click(function (event) {
    event.preventDefault();
    $.ajax({
        type: "post",
        dataType: "html",
        url: $(this).attr("rel"),
        data: { 
            "__RequestVerificationToken":
            $("input[name=__RequestVerificationToken]").val() 
        },
        success: function (response) {
            // ....
        }
    });
});
5 голосов
/ 06 мая 2011

Вы также можете сделать это:

$("a.markAsDone").click(function (event) {
    event.preventDefault();

    $.ajax({
        type: "post",
        dataType: "html",
        url: $(this).attr("rel"),
        data: $('<form>@Html.AntiForgeryToken()</form>').serialize(),
        success: function (response) {
        // ....
        }
    });
});

Это использует Razor, но если вы используете синтаксис WebForms, вы также можете использовать <%= %> теги

4 голосов
/ 07 сентября 2013

В дополнение к моему комментарию против ответа @ JBall, который помог мне на этом пути, это последний ответ, который работает для меня.Я использую MVC и Razor и отправляю форму с использованием jQuery AJAX, чтобы я мог обновить частичное представление с некоторыми новыми результатами, и я не хотел делать полный постбэк (и мерцание страницы).

Добавьте @Html.AntiForgeryToken() внутри формы как обычно.

Мой код кнопки отправки AJAX (т.е. событие onclick):

//User clicks the SUBMIT button
$("#btnSubmit").click(function (event) {

//prevent this button submitting the form as we will do that via AJAX
event.preventDefault();

//Validate the form first
if (!$('#searchForm').validate().form()) {
    alert("Please correct the errors");
    return false;
}

//Get the entire form's data - including the antiforgerytoken
var allFormData = $("#searchForm").serialize();

// The actual POST can now take place with a validated form
$.ajax({
    type: "POST",
    async: false,
    url: "/Home/SearchAjax",
    data: allFormData,
    dataType: "html",
    success: function (data) {
        $('#gridView').html(data);
        $('#TestGrid').jqGrid('setGridParam', { url: '@Url.Action("GetDetails", "Home", Model)', datatype: "json", page: 1 }).trigger('reloadGrid');
    }
});

Я оставил действие "success" впоскольку он показывает, как обновляется частичное представление, содержащее MvcJqGrid, и как оно обновляется (очень мощная сетка jqGrid, и это великолепная оболочка MVC для нее).

Мой метод контроллера выглядит следующим образом:

    //Ajax SUBMIT method
    [ValidateAntiForgeryToken]
    public ActionResult SearchAjax(EstateOutlet_D model) 
    {
        return View("_Grid", model);
    }

Я должен признать, что не являюсь поклонником размещения сообщений всей формы в качестве модели, но если вам нужно сделать это, то это один из способов, который работает.MVC просто делает привязку данных слишком легкой, поэтому вместо подстановки 16 отдельных значений (или слабо типизированной формы FormCollection) это нормально, я полагаю.Если вы знаете лучше, пожалуйста, дайте мне знать, так как я хочу создать надежный код MVC C #.

4 голосов
/ 08 июля 2015

нашел эту очень умную идею из https://gist.github.com/scottrippey/3428114 для каждого вызова $ .ajax, он изменяет запрос и добавляет токен.

// Setup CSRF safety for AJAX:
$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
    if (options.type.toUpperCase() === "POST") {
        // We need to add the verificationToken to all POSTs
        var token = $("input[name^=__RequestVerificationToken]").first();
        if (!token.length) return;

        var tokenName = token.attr("name");

        // If the data is JSON, then we need to put the token in the QueryString:
        if (options.contentType.indexOf('application/json') === 0) {
            // Add the token to the URL, because we can't add it to the JSON data:
            options.url += ((options.url.indexOf("?") === -1) ? "?" : "&") + token.serialize();
        } else if (typeof options.data === 'string' && options.data.indexOf(tokenName) === -1) {
            // Append to the data string:
            options.data += (options.data ? "&" : "") + token.serialize();
        }
    }
});
...