Web API 和 Angular 中的 XSRF
在 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 =>
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 日 - 初始版本