具有SPA架构的ValidateAntiForgeryToken

时间:2013-03-14 17:33:29

标签: asp.net-mvc-4 knockout.js single-page-application durandal hottowel

我正在设置注册和登录Hot Towel SPA应用程序。我已根据asp.net single page application template创建了SimpleMembershipFilters和ValidateHttpAntiForgeryTokenAttribute。

你如何得到

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

在$ ajax调用中如何传递AntiForgeryToken?另外我如何创建令牌?

4 个答案:

答案 0 :(得分:7)

关于如何使用javascript使用防伪令牌,我会阅读this article。本文是为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调用中将其作为参数数据参数传递。那么你将很难获得所有不同场景的防伪令牌。因为您必须反序列化主体然后找到令牌。在标题中,它非常一致且易于检索。


****编辑ray **

以下是一个动作过滤器的示例,您可以使用该过滤器来定义web api方法,以验证是否提供了antiforgerytoken。

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

答案 1 :(得分:3)

获取JS var

中令牌的值
var antiForgeryToken = $('input[name="__RequestVerificationToken"]').val();

然后在 .ajax 调用的 beforeSend 函数中添加你的ajax POST标题

beforeSend: function (xhr, settings) {
            if (settings.data != "") {
                settings.data += '&';
            }
            settings.data += '__RequestVerificationToken=' +  encodeURIComponent(antiForgeryToken);
}

答案 2 :(得分:1)

我对此有点挣扎,因为现有的答案似乎都不适用于基于热毛巾模板的Durandal SPA应用程序。

我不得不使用Evan Larson和curtisk的答案组合来获得一些像我认为的那样有效的东西。

到我的index.cshtml页面(Durandal支持cshtml和html)我在</body>标签上方添加了以下内容

@AntiForgery.GetHtml();

我添加了Evan Larson建议的自定义过滤器类,但是我必须修改它以支持单独查找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());
}

在我的Global.asax下的Application_Start中,我添加了

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和隐藏的表单值以便能够验证自己,这将更难以获取。

答案 3 :(得分:0)

如果使用MVC 5阅读此解决方案!

我尝试了上述解决方案,但它们对我不起作用,Action Filter从未到达,我无法弄清楚原因。上面没有提到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.Mvcusing 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
});