带有 SPA 架构的 ValidateAntiForgeryToken

Posted

技术标签:

【中文标题】带有 SPA 架构的 ValidateAntiForgeryToken【英文标题】:ValidateAntiForgeryToken with SPA architecture 【发布时间】:2013-03-03 05:09:14 【问题描述】:

我正在尝试为 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?另外我如何创建令牌?

【问题讨论】:

【参考方案1】:

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


**为光线编辑

这里是一个动作过滤器示例,您可以使用它来确定 Web 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();
        
    

【讨论】:

你怎么称呼ValidateRequestHeader 我会创建一个动作过滤器,因为这是一个横切关注点并且是面向方面的设计。通过这种方式,您只需将要强制执行此安全性的方法归于属性。 仅供参考:该属性实现了IAuthorizationFilter,但缺少public void OnAuthorization( AuthorizationContext filterContext ) 方法。 这种方法的限制是:页面必须是 Razor 页面 .cshtml 并且我的 SPA 页面/视图是简单的 HTML 页面,因此它对我不起作用。 Jaider,这个概念应该适用于你想要的任何场景。是的,代码采用了这些假设,但概念保持不变。您在服务器上创建一个防伪令牌。根据请求将其传递给客户端。客户端必须在任何 PUT/POST/DELETE http 请求中使用此令牌,方法是将此令牌放在 http 标头中。这可以防止跨站点请求伪造。【参考方案2】:

在JS var中获取token的值

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

然后只需在 .ajax 调用的 beforeSend 函数中添加您的 ajax POST 标头

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

【讨论】:

我必须添加到我的代码中的 _RequestVerfitcationToken 输入,但是如何以及在哪里可以设置输入的值?这是自动生成的东西吗?抱歉,这是新手。 是的,@Html.AntiForgeryToken() 会自动创建隐藏输入,这就是你在js中获取的值 因为我使用的是使用 Durandal 的 HotTowel 模板。 Durandal 遵循查找 .html 文件和 .js 文件的架构。 .html 文件是我们拥有所有 UI 元素的地方。 @ 调用在这里不可用,因为它是简单的 html 文件而不是 cshtml 文件。 默认可以直接使用html,但是在Durandal/Hot Towel下可以使用cshtml 啊,我们走了。我不知道。让我试试看。【参考方案3】:

我对此有点挣扎,因为对于我基于热毛巾模板的 Durandal SPA 应用,现有答案似乎都不能正常工作。

我必须结合使用 Evan Larson 和 curtisk 的答案,才能得到我认为应该的方式。

在我的 index.cshtml 页面(Durandal 支持 cshtml 和 html)我在 &lt;/body&gt; 标记上方添加了以下内容

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

这导致我的所有发布请求都需要一个验证令牌,该验证令牌基于 AntiForgery.GetHtml() 创建的 cookie 和隐藏表单验证令牌;

据我了解,这将防止跨站点脚本攻击的可能性,因为攻击站点需要能够提供 cookie 和隐藏表单值才能验证自己,而这将更难获得.

【讨论】:

我需要修改以使其正常工作:将 AntiForgeryConfig.CookieName 中的 cookiename 设置为 __RequestVerificationToken(在我的系统上称为 __RequestVerificationToken_fugh485) 另外,tokenCookie.Cookies.First().Value;给了我错误的饼干。改为 tokenCookie.Cookies.First(c => c.Name == "__RequestVerificationToken").Value;【参考方案4】:

如果使用 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.Mvcusing System.Web.Mvc.Filters,而不是 http 库(我认为这是 MVC v5 改变的事情之一。--

然后只需将过滤器[ValidateJSONAntiForgeryHeader] 应用于您的操作(或控制器),它应该会被正确调用。

&lt;/body&gt; 正上方的布局页面中,我有@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
);

【讨论】:

以上是关于带有 SPA 架构的 ValidateAntiForgeryToken的主要内容,如果未能解决你的问题,请参考以下文章

《SPA设计与架构》之认识SPA

SPA设计架构

微服务架构中的 SPA 认证

SPA设计与架构-理解单页面Web应用 (埃米顿.A斯科特) 中文pdf扫描版

带有 SPA 应用程序和 API 的会话

基于事件驱动架构构建微服务第15部分:SPA前端