【问题标题】:Web API and ValidateAntiForgeryTokenWeb API 和 ValidateAntiForgeryToken
【发布时间】:2012-07-13 16:21:51
【问题描述】:

我们有一些现有的 MVC Web 服务,它们从网页中称为 AJAX 样式。这些服务利用 ValidateAntiForgeryToken 属性来帮助防止请求伪造。

我们正在寻求将这些服务迁移到 Web API,但似乎没有等效的防伪功能。

我错过了什么吗?是否有其他方法可以使用 Web API 解决请求伪造问题?

【问题讨论】:

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


【解决方案1】:

再想一想之后,将 cookie 和表单令牌混合在一起是个坏主意,因为它违背了防伪令牌的全部目的。最好将 cookie 部分保留为 cookie,同时将表单部分移动到 auth 标头,因此这个新答案(再次作为 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>

【讨论】:

    【解决方案2】:

    Oswaldo 的回答,但作为 AuthorizeAttribute 实现

      [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
      public class ApiValidateAntiForgeryToken : AuthorizeAttribute
      {
        public static string GenerateAntiForgeryTokenForHeader() {
          string cookieToken, formToken;
          AntiForgery.GetTokens(null, out cookieToken, out formToken);
          return cookieToken + ":" + formToken;
        }
    
    
        protected override bool IsAuthorized(HttpActionContext actionContext) {
          var headers = actionContext.Request.Headers;
    
          // we pass both the cookie and the form token into a single header field
          string headerToken = headers.Contains("__RequestVerificationToken") ? headers.GetValues("__RequestVerificationToken").FirstOrDefault() : null;
    
          if (headerToken == null) {
            return false;
          }
    
          string[] tokens = headerToken.Split(':');
          if (tokens.Length != 2) {
            return false;
          }
    
          string cookieToken = tokens[0].Trim();
          string formToken = tokens[1].Trim();
    
          try {
            AntiForgery.Validate(cookieToken, formToken);
          }
          catch {
            return false;
          }
    
          return base.IsAuthorized(actionContext);
        }
      }
    

    您可以使用 [ApiValidateAntiForgeryToken] 装饰您的控制器或方法,然后将 RequestVerificationToken: "@ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader()" 作为 razor javascript 代码中方法的标头传递。

    【讨论】:

    • 嗨,@Javier,请您解释一下为什么您的ApiValidateAntiForgeryToken 实现继承自AuthorizeAttribute,而不是实现IAuthorizationFilter
    • 嗨 Gerardo,基本上它是相同的东西,但以不同的方式实现。请参阅stackoverflow.com/questions/27021506/… - 在我的情况下,我需要它用于需要 AutorizeAttribute 并且不能使用接口的项目(我认为这是因为 SignalR,但我不记得了)。无论如何,请考虑我在同一篇文章中给出的其他答案,因为它更安全。
    【解决方案3】:

    补充以上代码 过滤属性

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
        public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
        {
            public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(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 e)
                {
                    actionContext.Response = new HttpResponseMessage
                    {
                        StatusCode = HttpStatusCode.Forbidden,
                        RequestMessage = actionContext.ControllerContext.Request
                    };
                    return FromResult(actionContext.Response);
                }
                return continuation();
            }
    
            private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
            {
                var source = new TaskCompletionSource<HttpResponseMessage>();
                source.SetResult(result);
                return source.Task;
            }
    

    使用 Razor 的 HTML 函数

    @functions{
        public string TokenHeaderValue()
            {
                string cookieToken, formToken;
                AntiForgery.GetTokens(null, out cookieToken, out formToken);
                return cookieToken + ":" + formToken;
            }
    }
    

    使用角度

    return $http({
       method: 'POST',
       url: '@Url.Content("~/api/invite/")',
       data: {},
       headers: {
           'RequestVerificationToken': '@TokenHeaderValue()'
       }
    });
    

    【讨论】:

    • 你至少应该提供参考。原文在这里asp.net/web-api/overview/security/…
    • 为什么这比答案更好?
    • 我不相信这行得通(你复制的原件也行不通)。 cookieToken 必须作为 httpOnly cookie 发送,以便客户端脚本无法对其进行操作。通过简单地将两个标记连接在一起形成一个标记,您就允许 JavaScript 操作这两个标记,因此您正在破坏该机制的整个对象。
    • 嗨,@Oswaldo,请你解释一下为什么ValidateAntiForgeryTokenAttribute 实现IAuthorizationFilter,而不是IAuthenticationFilter
    • @Andy 问题是 谁的 javascript?这种方法依赖于第三方恶意网站的javascript无法访问cookie,而受保护网站的javascript可以访问cookie。受保护网站页面上的 XSS 漏洞将通过允许攻击者访问 cookie 来解决此问题。因此,防止 XSS 是这种方法的关键。
    【解决方案4】:

    这个link 有帮助,您可以从剃刀视图中检索防伪令牌并将令牌作为标头传递:

    var csrfToken = $("input[name='__RequestVerificationToken']").val(); 
    $.ajax({
        headers: { __RequestVerificationToken: csrfToken },
        type: "POST",
        dataType: "json",
        contentType: 'application/json; charset=utf-8',
        url: "/api/products",
        data: JSON.stringify({ name: "Milk", price: 2.33 }),
        statusCode: {
            200: function () {
                alert("Success!");
            }
        }
    });
    

    【讨论】:

    • "ValidateAjaxAntiForgeryToken 仅在用户通过身份验证时有效,不适用于匿名请求。"
    • @Rudey - 嗯。你确定吗?
    • 是的,这是答案中链接帖子的直接引用。
    【解决方案5】:

    你可以实现这样的授权属性:

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            try
            {
                AntiForgery.Validate();
            }
            catch
            {
                actionContext.Response = new HttpResponseMessage 
                { 
                    StatusCode = HttpStatusCode.Forbidden, 
                    RequestMessage = actionContext.ControllerContext.Request 
                };
                return FromResult(actionContext.Response);
            }
            return continuation();
        }
    
        private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
        {
            var source = new TaskCompletionSource<HttpResponseMessage>();
            source.SetResult(result);
            return source.Task;
        }
    }
    

    然后用它装饰您的 API 操作:

    [ValidateAntiForgeryToken]
    public HttpResponseMessage Post()
    {
        // some work
        return Request.CreateResponse(HttpStatusCode.Accepted);
    }
    

    【讨论】:

    • 谢谢,System.Web.Helpers.AntiForgery 看起来可以解决我的问题。
    • 在我的情况下,数据通过以下代码以 JSON 字符串的形式出现,上述解决方案不起作用: $.ajax({ url: url, method: "PUT", contentType: " application/json", dataType: "json", data: formJsonData })
    • 我认为这种方法只有在您使用表单编码发布到 ajax 服务并在 ajax 请求表单数据中明确包含令牌时才有效。如果您想为您的 ajax 数据使用 JSON,您将需要滚动您自己的代码来提取令牌并调用 AntiForgery.Validate 的两参数重载。您可以在 json 数据或类似 stephenwalther.com/archive/2013/03/05/… 的 HTTP 标头中发送“formToken”参数
    • @Darin 如果不引用System.Web.WebPages 就不可能拥有 AntiForgery,我说得对吗?因为我真的不想让我的 WebAPI 库引用 WebPages...
    • @RuudLenders AntiForgery 位于Microsoft.AspNet.WebPages,显然依赖于WebPagesSystem.Web.Razor。但是,使用 API 是不可能的,因为在这种情况下,由于没有页面,所以不可能在页面上注入防伪令牌。
    猜你喜欢
    • 1970-01-01
    • 2018-02-21
    • 2018-03-12
    • 1970-01-01
    • 1970-01-01
    • 2013-03-23
    • 2020-08-18
    • 2015-11-24
    • 1970-01-01
    相关资源
    最近更新 更多