【问题标题】:Problems implementing ValidatingAntiForgeryToken attribute for Web API with MVC 4 RC使用 MVC 4 RC 为 Web API 实现 ValidatingAntiForgeryToken 属性的问题
【发布时间】:2012-07-28 09:15:47
【问题描述】:

我正在发出基于 JSON 的 AJAX 请求,对于 MVC 控制器,我非常感谢 Phil Haack 的 Preventing CSRF with AJAX 和 Johan Driessen 的 Updated Anti-XSRF for MVC 4 RC。但是,当我将以 API 为中心的控制器转换为 Web API 时,我遇到了两种方法之间的功能明显不同的问题,并且我无法转换 CSRF 代码。

ScottS 最近提出了一个类似的 question,它是 Darin Dimitrov 的 answered。 Darin 的解决方案包括实现一个调用 AntiForgery.Validate 的授权过滤器。不幸的是,这段代码对我不起作用(见下一段),老实说,对我来说太先进了。

据我了解,Phil 的解决方案在没有表单元素的情况下发出 JSON 请求时克服了 MVC AntiForgery 的问题;表单元素由 AntiForgery.Validate 方法假定/预期。我相信这可能就是我对 Darin 的解决方案也有问题的原因。我收到 HttpAntiForgeryException “所需的防伪表单字段 '__RequestVerificationToken' 不存在”。我确定令牌正在被发布(尽管根据 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 的解决方案与 Darin 的解决方案混合在一起进行破解,并且能够让事情正常运行,但我正在引入 HttpContext.Current,不确定这是否合适/安全以及为什么我不能使用提供的 HttpActionContext。

这是我不雅的混搭。变化是 try 块中的 2 行:

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 的解决方案假设存在表单元素是否正确?
  • 将 Darin 的 Web API 过滤器与 Johan 的 MVC 4 RC 代码混搭的优雅方法是什么?

提前致谢!

【问题讨论】:

  • FromResult 是什么?

标签: asp.net-mvc-4 asp.net-web-api antiforgerytoken


【解决方案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

【讨论】:

  • 嘿...GetCookies() 方法呢?一般来说,我们必须如何实现它?
  • 来自MVC Source code,我看到他们在GetFormToken方法中使用了httpContext.Request.Form[_config.FormFieldName]。有什么理由不使用 Request.Params 来访问表单和标头值?我认为它也可能包括 cookie :(。但是当表单不可用时,他们可以检查请求标头...
  • HttpRequestHeadersExtensions DLL System.Net.Http.Formatting.dll 也来自 Nuget 包 Microsoft.AspNet.WebApi.Client。
  • 同样,AntiForgery 和 AntiForgeryConfig 来自 Microsoft.AspNet.WebPages nuget 包中的 System.Web.WebPages.dll。
【解决方案2】:

只是想补充一点,这种方法也适用于我(.ajax 将 JSON 发布到 Web 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】:

    使用 Darin 的答案的扩展方法,并检查标题是否存在。检查意味着生成的错误消息更能说明问题所在(“所需的防伪表单字段“__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] 装饰你的控制器或方法

      并将其添加到 razor 文件中以生成您的 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 =&gt; o.HeaderName = "__RequestVerificationToken")

        【讨论】:

          猜你喜欢
          • 2012-06-24
          • 2013-01-24
          • 1970-01-01
          • 1970-01-01
          • 2012-06-18
          • 1970-01-01
          • 2014-05-03
          • 1970-01-01
          • 2012-10-05
          相关资源
          最近更新 更多