65.9K
CodeProject 正在变化。 阅读更多。
Home

Web API 和 Angular 中的 XSRF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (6投票s)

2016年4月15日

CPOL

2分钟阅读

viewsIcon

39304

downloadIcon

711

在 Web API 和 Angular 中使用 XSRF

引言

在本文中,我将展示如何将 ASP.NET MVC 中常用的反伪造请求(antiforgery)基础设施与 ASP.NET Web API 和 Angular 结合使用。

背景

首先,我们来了解一下反 XSRF 基础设施在 MVC 中的工作原理。当 Razor 标记中出现 Html.AntiForgeryToken 行时,会发生两件事:在 HTML 中,会有一个隐藏的输入元素,用于存储令牌的一半,并且还会将包含另一半令牌的 cookie 附加到响应中。之后,当用户提交表单时,隐藏字段中的令牌将位于请求体中,cookie 自然会作为 cookie 发送。:) 在服务器端,AntiForgeryToken 类将负责验证令牌是否正确。

好的,现在让我们切换到 SPA。在这种情况下,我们没有服务器端令牌,因为标记将在客户端生成。我提出的解决方案如下:我创建了一个 Web API 端点,该端点使用“常规”AntiForgeryToken 类来生成令牌,并将这两个令牌作为响应体和 cookie 发送回去。我将使用 Angular 指令渲染令牌,并且一个拦截器会将此令牌作为 HTTP 标头附加。之后,在服务器端,一个自定义筛选器将负责验证令牌。

您可以找到附加到本文的解决方案,或者可以在 GitHub 上浏览它。

解决方案

服务器端代码将非常简单。有两种情况,如果这是本次会话中首次调用此端点,那么我们没有令牌。GetTokens 函数的第一个参数是 cookie 令牌,如果我们已经有了它,否则只需提供一个空字符串。重要的是,cookie 令牌将在会话的整个生命周期内保持不变,并且 GetTokens 如果我们提供现有的 cookie 值,将提供一个 null 值。

[HttpGet]
[Route("antiforgerytoken")]
public HttpResponseMessage GetAntiForgeryToken()
{
    HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);

    HttpCookie cookie = HttpContext.Current.Request.Cookies["xsrf-token"];

    string cookieToken;
    string formToken;
    AntiForgery.GetTokens(cookie == null ? "" : cookie.Value, out cookieToken, out formToken);

    AntiForgeryTokenModel content = new AntiForgeryTokenModel
    {
        AntiForgeryToken = formToken
    };

    response.Content = new StringContent(
             JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json");

    if (!string.IsNullOrEmpty(cookieToken))
    {
        response.Headers.AddCookies(new[]
        {
            new CookieHeaderValue("xsrf-token", cookieToken)
            {
                Expires = DateTimeOffset.Now.AddMinutes(10),
                Path = "/"
            }
        });
    }

    return response;
}

接下来是 Angular 指令。它将调用端点,然后像 MVC 通常那样将令牌渲染为隐藏的输入元素。

(function() {
    'use strict';

    function antiForgeryDirectiveController(appService) {
        var directive = this;

        directive.antiForgeryToken = '';

        directive.activate = function () {
            appService.getAntiForgeryToken().then(function(data) {
                directive.antiForgeryToken = data.antiForgeryToken;;
            });
        };

        directive.activate();
    }

    function antiForgeryTokenDirective() {
        return {
            scope: {},
            controllerAs: 'directive',
            template: '<input id="__antiForgeryToken" 
            name="antiForgeryToken" type="hidden" 
            value="{{directive.antiForgeryToken}}" />',
            controller: [ 'appService', antiForgeryDirectiveController ]
        }
    }

    angular.module('demoApp').directive('antiforgerytoken', antiForgeryTokenDirective);
})();

我们在客户端还有另一项工作。如果 HTML 包含隐藏的输入元素(我们可以通过 id 识别它),那么我们需要将 HTTP 标头添加到请求中。为此,我们将使用一个拦截器

(function() {
    'use strict';

    function antiForgeryInterceptor() {
        return {
            request: function($config) {
                var antiForgeryTokenField = document.getElementById('__antiForgeryToken');
                if (antiForgeryTokenField) {
                    var xsrfToken = antiForgeryTokenField.value;
                    $config.headers['XSRF-TOKEN'] = xsrfToken;
                }

                return $config;
            }
        };
    }

    angular.module('demoApp').service('antiForgeryInterceptor', antiForgeryInterceptor);
})();

最后一步是服务器端验证

public sealed class ValidateAntiForgeryTokenFilter : ActionFilterAttribute
{
    private const string XsrfHeader = "XSRF-TOKEN";
    private const string XsrfCookie = "xsrf-token";

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        HttpRequestHeaders headers = actionContext.Request.Headers;
        IEnumerable xsrfTokenList;

        if (!headers.TryGetValues(XsrfHeader, out xsrfTokenList))
        {
            actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);
            return;
        }

        string tokenHeaderValue = xsrfTokenList.First();

        CookieState tokenCookie = actionContext.Request.Headers.GetCookies().Select(c =&gt; 
                                  c[XsrfCookie]).FirstOrDefault();

        if (tokenCookie == null)
        {
            actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);
            return;
        }

        try
        {
            AntiForgery.Validate(tokenCookie.Value, tokenHeaderValue);
        }
        catch (HttpAntiForgeryException)
        {
            actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);
        }
    }
}

之后,我们可以像使用原始的 ValidateAntyForgeryToken 属性一样使用此筛选器。

历史

  • 2016 年 4 月 15 日 - 初始版本
© . All rights reserved.