ValidateAntiForgeryToken с архитектурой SPA

Я пытаюсь установить регистрацию и вход для подачи заявки на Hot Towel SPA. Я создал SimpleMembershipFilters и ValidateHttpAntiForgeryTokenAttribute на основе шаблона одностраничного приложения asp.net.

Как вы получаете

 @Html.AntiForgeryToken()

код для работы в шаблоне Durandal SPA.

В настоящее время у меня есть register.html

<section>
    <h2 data-bind="text: title"></h2>

    <label>Firstname:</label><input data-bind="value: firstName" type="text"  />
    <label>Lastname:</label><input data-bind="value: lastName" type="text"  />
    <label>Email:</label><input data-bind="value: emailAddress" type="text"  />
    <label>Company:</label><input data-bind="value: company" type="text"  />
    <br />
    <label>Password:</label><input data-bind="value: password1" type="password" />
    <label>Re-Enter Password:</label><input data-bind="value: password2" type="password" />
    <input type="button" value="Register" data-bind="click: registerUser" class="btn" />
</section>

register.js:

define(['services/logger'], function (logger) {
    var vm = {
        activate: activate,
        title: 'Register',
        firstName: ko.observable(),
        lastName: ko.observable(),
        emailAddress: ko.observable(),
        company: ko.observable(),
        password1: ko.observable(),
        password2: ko.observable(),
        registerUser: function () {
            var d = {
                'FirstName': vm.firstName,
                'LastName': vm.lastName,
                'EmailAddress': vm.emailAddress,
                'Company': vm.company,
                'Password': vm.password1,
                'ConfirmPassword': vm.password2
            };
            $.ajax({
                url: 'Account/JsonRegister',
                type: "POST",
                data: d ,
                success: function (result) {
                },
                error: function (result) {
                }
            });
        },
    };


    return vm;

    //#region Internal Methods
    function activate() {
        logger.log('Login Screen Activated', null, 'login', true);
        return true;
    }
    //#endregion
});

Как передать AntiForgeryToken в вызове $ajax? Также как мне создать токен?


person jmogera    schedule 14.03.2013    source источник


Ответы (4)


Я бы прочитал эту статью о том, как использовать токены защиты от подделки с помощью javascript. Статья написана для WebApi, но при желании ее можно легко применить к контроллеру MVC.

Краткий ответ примерно такой: Внутри вашего представления cshtml:

<script>
    @functions{
        public string TokenHeaderValue()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            return cookieToken + ":" + formToken;                
        }
    }

    $.ajax("api/values", {
        type: "post",
        contentType: "application/json",
        data: {  }, // JSON data goes here
        dataType: "json",
        headers: {
            'RequestVerificationToken': '@TokenHeaderValue()'
        }
    });
</script>

Затем внутри вашего контроллера asp.net вам нужно проверить токен следующим образом:

void ValidateRequestHeader(HttpRequestMessage request)
{
    string cookieToken = "";
    string formToken = "";

    IEnumerable<string> tokenHeaders;
    if (request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
    {
        string[] tokens = tokenHeaders.First().Split(':');
        if (tokens.Length == 2)
        {
            cookieToken = tokens[0].Trim();
            formToken = tokens[1].Trim();
        }
    }
    AntiForgery.Validate(cookieToken, formToken);
}

Причина, по которой вы хотите передать его в заголовках, заключается в том, что если вы передаете его в качестве параметра данных параметра в своем вызове ajax, внутри строки запроса или тела запроса. Тогда вам будет сложнее получить токен антифальсификации для всех ваших различных сценариев. Потому что вам придется десериализовать тело, а затем найти токен. В заголовках это довольно последовательно и легко извлекается.


**изменить для луча

Вот пример фильтра действий, который можно использовать для атрибутирования методов веб-API для проверки наличия маркера защиты от подделки.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Helpers;
using System.Web.Http.Filters;
using System.Net.Http;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Threading;

namespace PAWS.Web.Classes.Filters
{
    public class ValidateJsonAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException("HttpActionContext");
            }

            if (actionContext.Request.Method != HttpMethod.Get)
            {
                return ValidateAntiForgeryToken(actionContext, cancellationToken, continuation);
            }

            return continuation();
        }

        private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
        {
            var source = new TaskCompletionSource<HttpResponseMessage>();
            source.SetResult(result);
            return source.Task;
        }

        private Task<HttpResponseMessage> ValidateAntiForgeryToken(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            try
            {
                string cookieToken = "";
                string formToken = "";
                IEnumerable<string> tokenHeaders;
                if (actionContext.Request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
                {
                    string[] tokens = tokenHeaders.First().Split(':');
                    if (tokens.Length == 2)
                    {
                        cookieToken = tokens[0].Trim();
                        formToken = tokens[1].Trim();
                    }
                }
                AntiForgery.Validate(cookieToken, formToken);
            }
            catch (System.Web.Mvc.HttpAntiForgeryException ex)
            {
                actionContext.Response = new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.Forbidden,
                    RequestMessage = actionContext.ControllerContext.Request
                };
                return FromResult(actionContext.Response);
            }
            return continuation();
        }
    }
}
person Evan Larsen    schedule 19.03.2013
comment
Как вы называете ValidateRequestHeader? - person Ray Cheng; 22.03.2013
comment
Я бы создал фильтр действий, потому что это сквозная задача и аспектно-ориентированный дизайн. Таким образом, вы можете просто указать методы, для которых вы хотите применить эту безопасность. - person Evan Larsen; 22.03.2013
comment
К вашему сведению: атрибут реализует IAuthorizationFilter, но метод public void OnAuthorization( AuthorizationContext filterContext ) отсутствует. - person Jaans; 31.07.2013
comment
Ограничение этого подхода заключается в следующем: страница должна быть страницей Razor .cshtml, а мои страницы/представления SPA — это простые HTML-страницы, поэтому у меня это не сработает. - person Jaider; 25.06.2014
comment
Джайдер, концепция должна работать для любого сценария, который вы хотите. Да, код принимает эти предположения, но концепции остаются неизменными. Вы создаете токен защиты от подделки на сервере. Передать клиенту по запросу. Клиент должен использовать этот токен в любых http-запросах PUT/POST/DELETE, помещая этот токен в заголовок http. Это предотвращает подделку межсайтовых запросов. - person Evan Larsen; 25.06.2014

Получить значение токена в JS var

var antiForgeryToken = $('input[name="__RequestVerificationToken"]').val();

Затем просто добавьте в заголовки POST ajax функцию beforeSend вызов .ajax

beforeSend: function (xhr, settings) {
            if (settings.data != "") {
                settings.data += '&';
            }
            settings.data += '__RequestVerificationToken=' +  encodeURIComponent(antiForgeryToken);
}
person curtisk    schedule 14.03.2013
comment
ввод _RequestVerfitcationToken я должен добавить в свой код, но как и где можно установить значение ввода? Это то, что автоматически генерируется? Извините, новичок в этом. - person jmogera; 14.03.2013
comment
Да, @Html.AntiForgeryToken() автоматически создаст скрытый ввод, это то, что вы получаете значение в js - person curtisk; 14.03.2013
comment
Поскольку я использую шаблон HotTowel, в котором используется Durandal. Durandal следует архитектуре, в которой он ищет файлы .html и .js. В файле .html находятся все элементы пользовательского интерфейса. Вызов @ здесь недоступен, так как это файл html, а не файл cshtml. - person jmogera; 14.03.2013
comment
По умолчанию он может использовать прямой html, но вы можете использовать cshtml под Durandal/Hot Towel. - person curtisk; 14.03.2013
comment
ну вот и мы. Я этого не знал. Позвольте мне попробовать это. - person jmogera; 14.03.2013
comment
Я создал файл Razor .cshtml, но ни один из Html.Helpers не распознается. Ошибка 15 «System.Web.WebPages.Html.HtmlHelper» не содержит определения для «AntiForgeryToken», и не удалось найти метод расширения «AntiForgeryToken», принимающий первый аргумент типа «System.Web.WebPages.Html.HtmlHelper» ( вам не хватает директивы using или ссылки на сборку?) - person jmogera; 15.03.2013

Я немного боролся с этим, поскольку ни один из существующих ответов, похоже, не работал правильно в случае моего приложения Durandal SPA, основанного на шаблоне Hot Towel.

Мне пришлось использовать комбинацию ответов Эвана Ларсона и Куртиска, чтобы получить что-то, что работало так, как я думаю.

На мою страницу index.cshtml (Durandal поддерживает cshtml наряду с html) я добавил следующее чуть выше тега </body>

@AntiForgery.GetHtml();

Я добавил пользовательский класс фильтра, предложенный Эваном Ларсоном, однако мне пришлось изменить его, чтобы поддерживать поиск значения cookie отдельно и использовать __RequestVerificationToken в качестве имени, а не RequestVerificationToken, поскольку это то, что предоставляется AntiForgery.GetHtml();

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Helpers;
using System.Web.Http.Filters;
using System.Net.Http;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Threading;
using System.Net.Http.Headers;

namespace mySPA.Filters
{
    public class ValidateJsonAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException("HttpActionContext");
            }

            if (actionContext.Request.Method != HttpMethod.Get)
            {
                return ValidateAntiForgeryToken(actionContext, cancellationToken, continuation);
            }

            return continuation();
        }

        private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
        {
            var source = new TaskCompletionSource<HttpResponseMessage>();
            source.SetResult(result);
            return source.Task;
        }

        private Task<HttpResponseMessage> ValidateAntiForgeryToken(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            try
            {
                string cookieToken = "";
                string formToken = "";
                IEnumerable<string> tokenHeaders;
                if (actionContext.Request.Headers.TryGetValues("__RequestVerificationToken", out tokenHeaders))
                {
                    formToken = tokenHeaders.First();
                }
                IEnumerable<CookieHeaderValue> cookies = actionContext.Request.Headers.GetCookies("__RequestVerificationToken");
                CookieHeaderValue tokenCookie = cookies.First();
                if (tokenCookie != null)
                {
                    cookieToken = tokenCookie.Cookies.First().Value;
                }
                AntiForgery.Validate(cookieToken, formToken);
            }
            catch (System.Web.Mvc.HttpAntiForgeryException ex)
            {
                actionContext.Response = new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.Forbidden,
                    RequestMessage = actionContext.ControllerContext.Request
                };
                return FromResult(actionContext.Response);
            }
            return continuation();
        }
    }
}

Впоследствии в моем App_Start/FilterConfig.cs я добавил следующее

public static void RegisterHttpFilters(HttpFilterCollection filters)
{
    filters.Add(new ValidateJsonAntiForgeryTokenAttribute());
}

В Application_Start под моим Global.asax я добавил

FilterConfig.RegisterHttpFilters(GlobalConfiguration.Configuration.Filters);

Наконец, для моих ajax-вызовов я добавил вывод поиска входных данных curtisk, чтобы добавить заголовок к моему ajax-запросу, в случае запроса на вход.

var formForgeryToken = $('input[name="__RequestVerificationToken"]').val();

return Q.when($.ajax({
    url: '/breeze/account/login',
    type: 'POST',
    contentType: 'application/json',
    dataType: 'json',
    data: JSON.stringify(data),
    headers: {
        "__RequestVerificationToken": formForgeryToken
    }
})).fail(handleError);

Это приводит к тому, что все мои почтовые запросы требуют токена проверки, который основан на маркерах проверки cookie и скрытой формы, созданных AntiForgery.GetHtml();

Насколько я понимаю, это предотвратит потенциальные атаки с использованием межсайтовых сценариев, поскольку атакующий сайт должен будет предоставить как файл cookie, так и значение скрытой формы, чтобы иметь возможность проверить себя, что было бы гораздо сложнее получить.

person Denis Pitcher    schedule 27.08.2013
comment
Мне нужно было изменить, чтобы следовать, чтобы это работало: установите имя файла cookie в AntiForgeryConfig.CookieName на __RequestVerificationToken (в моей системе он назывался __RequestVerificationToken_fugh485) - person Nicow; 16.01.2017
comment
Кроме того, tokenCookie.Cookies.First().Value; дал мне не то печенье. Изменено на tokenCookie.Cookies.First(c =› c.Name == __RequestVerificationToken).Value; - person Nicow; 16.01.2017

Если вы используете MVC 5, прочтите это решение!

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Filters;

namespace SydHeller.Filters
{
    public class ValidateJSONAntiForgeryHeader : FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            string clientToken = filterContext.RequestContext.HttpContext.Request.Headers.Get(KEY_NAME);
            if (clientToken == null)
            {
                throw new HttpAntiForgeryException(string.Format("Header does not contain {0}", KEY_NAME));
            }

            string serverToken = filterContext.HttpContext.Request.Cookies.Get(KEY_NAME).Value;
            if (serverToken == null)
            {
                throw new HttpAntiForgeryException(string.Format("Cookies does not contain {0}", KEY_NAME));
            }

            System.Web.Helpers.AntiForgery.Validate(serverToken, clientToken);
        }

        private const string KEY_NAME = "__RequestVerificationToken";
    }
}

-- Обратите внимание на библиотеки using System.Web.Mvc и using System.Web.Mvc.Filters, а не на библиотеки http (я думаю, что это одна из вещей, которые изменились в MVC v5. --

Затем просто примените фильтр [ValidateJSONAntiForgeryHeader] к вашему действию (или контроллеру), и он должен быть вызван правильно.

На моей странице макета прямо над </body> у меня есть @AntiForgery.GetHtml();

Наконец, на моей странице Razor я делаю вызов ajax следующим образом:

var formForgeryToken = $('input[name="__RequestVerificationToken"]').val();

$.ajax({
  type: "POST",
  url: serviceURL,
  contentType: "application/json; charset=utf-8",
  dataType: "json",
  data: requestData,
  headers: {
     "__RequestVerificationToken": formForgeryToken
  },
     success: crimeDataSuccessFunc,
     error: crimeDataErrorFunc
});
person blubberbo    schedule 27.01.2017