Проблемы с реализацией атрибута ValidatingAntiForgeryToken для веб-API с MVC 4 RC
Я делаю запросы AJAX на основе JSON и с помощью MVC-контроллеров очень благодарен Phil Haack за его Предотвращение CSRF с AJAX и, Johan Driessen Обновлен Anti-XSRF для MVC 4 RC. Но по мере перехода API-ориентированных контроллеров в Web API я сталкиваюсь с проблемами, когда функциональность между этими двумя подходами заметно отличается, и я не могу перейти на код CSRF.
Скоттс недавно поднял аналогичный
question, который был
ответил Дарином Димитровым. Решение Darin включает в себя реализацию фильтра авторизации, который вызывает AntiForgery.Validate. К сожалению, этот код не работает для меня (см. Следующий абзац) и, честно говоря, слишком продвинутый для меня.
Как я понимаю, решение Фила преодолевает проблему с MVC AntiForgery при выполнении запросов JSON в отсутствие элемента формы; элемент формы предполагается/ожидаемым с помощью метода AntiForgery.Validate. Я считаю, что это может быть и потому, что у меня проблемы с решением Дарина. Я получаю исключение HttpAntiForgeryException "Необходимое поле формы для подделки" __RequestVerificationToken "нет". Я уверен, что токен POSTED (хотя и в заголовке для решения Phil Haack). Здесь снимок клиентского вызова:
$token = $('input[name=""__RequestVerificationToken""]').val();
$.ajax({
url:/api/states",
type: "POST",
dataType: "json",
contentType: "application/json: charset=utf-8",
headers: { __RequestVerificationToken: $token }
}).done(function (json) {
...
});
Я попробовал взломать, объединив решение Johan с Дарином и смог заставить все работать, но представляю HttpContext.Current, неуверенный, является ли это подходящим/безопасным и почему я не могу использовать предоставленный HttpActionContext.
Здесь мое неэлегантное mash-up.. изменение - это 2 строки в блоке try:
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
try
{
var cookie = HttpContext.Current.Request.Cookies[AntiForgeryConfig.CookieName];
AntiForgery.Validate(cookie != null ? cookie.Value : null, HttpContext.Current.Request.Headers["__RequestVerificationToken"]);
}
catch
{
actionContext.Response = new HttpResponseMessage
{
StatusCode = HttpStatusCode.Forbidden,
RequestMessage = actionContext.ControllerContext.Request
};
return FromResult(actionContext.Response);
}
return continuation();
}
Мои вопросы:
- Правильно ли я полагаю, что решение Дарина предполагает существование элемента формы?
- Какой элегантный способ скомпоновать фильтр Darin Web API с кодом RC Johan MVC 4?
Спасибо заранее!
Ответы
Ответ 1
Вы можете попробовать прочитать из заголовков:
var headers = actionContext.Request.Headers;
var cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
Примечание. GetCookies
- это метод расширения, существующий в классе HttpRequestHeadersExtensions
, который является частью System.Net.Http.Formatting.dll
. Скорее всего, это будет C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Net.Http.Formatting.dll
Ответ 2
Просто хотелось добавить, что этот подход работал и для меня (.ajax, отправляя JSON в конечную точку веб-API), хотя я немного упростил его, наследуя от ActionFilterAttribute и переопределив метод OnActionExecuting.
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var cookieName = AntiForgeryConfig.CookieName;
var headers = actionContext.Request.Headers;
var cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
}
catch
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
}
}
}
Ответ 3
Метод расширения с использованием ответа Дарина с проверкой наличия заголовка. Проверка означает, что результирующее сообщение об ошибке больше указывает на то, что неправильно ( "Требуемое поле формы анти-подделки" __RequestVerificationToken "нет".) Против "Данный заголовок не найден".
public static bool IsHeaderAntiForgeryTokenValid(this HttpRequestMessage request)
{
try
{
HttpRequestHeaders headers = request.Headers;
CookieState cookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName])
.FirstOrDefault();
var rvt = string.Empty;
if (headers.Any(x => x.Key == AntiForgeryConfig.CookieName))
rvt = headers.GetValues(AntiForgeryConfig.CookieName).FirstOrDefault();
AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
}
catch (Exception ex)
{
LogHelper.LogError(ex);
return false;
}
return true;
}
Использование ApiController:
public IHttpActionResult Get()
{
if (Request.IsHeaderAntiForgeryTokenValid())
return Ok();
else
return BadRequest();
}
Ответ 4
Реализация с использованием AuthorizeAttribute:
using System;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
using System.Web.Http.Controllers;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ApiValidateAntiForgeryToken : AuthorizeAttribute {
public const string HeaderName = "X-RequestVerificationToken";
private static string CookieName => AntiForgeryConfig.CookieName;
public static string GenerateAntiForgeryTokenForHeader(HttpContext httpContext) {
if (httpContext == null) {
throw new ArgumentNullException(nameof(httpContext));
}
// check that if the cookie is set to require ssl then we must be using it
if (AntiForgeryConfig.RequireSsl && !httpContext.Request.IsSecureConnection) {
throw new InvalidOperationException("Cannot generate an Anti Forgery Token for a non secure context");
}
// try to find the old cookie token
string oldCookieToken = null;
try {
var token = httpContext.Request.Cookies[CookieName];
if (!string.IsNullOrEmpty(token?.Value)) {
oldCookieToken = token.Value;
}
}
catch {
// do nothing
}
string cookieToken, formToken;
AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken);
// set the cookie on the response if we got a new one
if (cookieToken != null) {
var cookie = new HttpCookie(CookieName, cookieToken) {
HttpOnly = true,
};
// note: don't set it directly since the default value is automatically populated from the <httpCookies> config element
if (AntiForgeryConfig.RequireSsl) {
cookie.Secure = AntiForgeryConfig.RequireSsl;
}
httpContext.Response.Cookies.Set(cookie);
}
return formToken;
}
protected override bool IsAuthorized(HttpActionContext actionContext) {
if (HttpContext.Current == null) {
// we need a context to be able to use AntiForgery
return false;
}
var headers = actionContext.Request.Headers;
var cookies = headers.GetCookies();
// check that if the cookie is set to require ssl then we must honor it
if (AntiForgeryConfig.RequireSsl && !HttpContext.Current.Request.IsSecureConnection) {
return false;
}
try {
string cookieToken = cookies.Select(c => c[CookieName]).FirstOrDefault()?.Value?.Trim(); // this throws if the cookie does not exist
string formToken = headers.GetValues(HeaderName).FirstOrDefault()?.Trim();
if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken)) {
return false;
}
AntiForgery.Validate(cookieToken, formToken);
return base.IsAuthorized(actionContext);
}
catch {
return false;
}
}
}
Затем просто украсьте свой контроллер или методы с помощью [ApiValidateAntiForgeryToken]
И добавьте в файл бритвы это, чтобы сгенерировать токен для javascript:
<script>
var antiForgeryToken = '@ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader(HttpContext.Current)';
// your code here that uses such token, basically setting it as a 'X-RequestVerificationToken' header for any AJAX calls
</script>
Ответ 5
Если это кому-то помогает, в ядре .net значение заголовка по умолчанию на самом деле просто "RequestVerificationToken", без "__". Так что если вы измените ключ заголовка вместо этого, он будет работать.
Вы также можете переопределить имя заголовка, если хотите:
services.AddAntiforgery(o => o.HeaderName = "__RequestVerificationToken")