【问题标题】:How to secure an ASP.NET Web API [closed]如何保护 ASP.NET Web API [关闭]
【发布时间】:2012-07-31 07:32:17
【问题描述】:

我想使用 ASP.NET Web API 构建一个 RESTful 网络服务,第三方开发人员将使用它来访问我的应用程序的数据。

我已经阅读了很多关于 OAuth 的内容,它似乎是标准,但是找到一个很好的示例,其中包含解释其工作原理的文档(并且确实有效!)似乎令人难以置信困难(尤其是对于 OAuth 的新手)。

是否有一个实际构建和工作的示例并展示了如何实现它?

我已经下载了许多示例:

  • DotNetOAuth - 从新手的角度来看文档是毫无希望的
  • Thinktecture - 无法构建

我还看过一些博客,建议使用简单的基于令牌的方案(如 this)——这似乎是在重新发明轮子,但它确实具有概念上相当简单的优势。

在 SO 上似乎有很多这样的问题,但没有很好的答案。

这个空间里的每个人都在做什么?

【问题讨论】:

    标签: c# asp.net-mvc oauth asp.net-web-api


    【解决方案1】:

    更新:

    我已将此链接添加到我的其他答案 how to use JWT authentication for ASP.NET Web API 此处,供对 JWT 感兴趣的任何人使用。


    我们已设法将 HMAC 身份验证应用于保护 Web API,并且效果很好。 HMAC 身份验证为每个消费者使用一个密钥,消费者和服务器都知道对消息进行 hmac 哈希处理,应使用 HMAC256。大多数情况下,消费者的哈希密码被用作密钥。

    消息通常由 HTTP 请求中的数据构建,甚至是添加到 HTTP 标头的自定义数据,消息可能包括:

    1. 时间戳:发送请求的时间(UTC 或 GMT)
    2. HTTP 动词:GET、POST、PUT、DELETE。
    3. 发布数据和查询字符串,
    4. 网址

    在底层,HMAC 身份验证是:

    消费者向Web服务器发送HTTP请求,构建签名(hmac hash的输出)后,HTTP请求的模板:

    User-Agent: {agent}   
    Host: {host}   
    Timestamp: {timestamp}
    Authentication: {username}:{signature}
    

    GET 请求示例:

    GET /webapi.hmac/api/values
    
    User-Agent: Fiddler    
    Host: localhost    
    Timestamp: Thursday, August 02, 2012 3:30:32 PM 
    Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=
    

    散列得到签名的消息:

    GET\n
    Thursday, August 02, 2012 3:30:32 PM\n
    /webapi.hmac/api/values\n
    

    带有查询字符串的 POST 请求示例(下面的签名不正确,只是一个示例)

    POST /webapi.hmac/api/values?key2=value2
    
    User-Agent: Fiddler    
    Host: localhost    
    Content-Type: application/x-www-form-urlencoded
    Timestamp: Thursday, August 02, 2012 3:30:32 PM 
    Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=
    
    key1=value1&key3=value3
    

    散列得到签名的消息

    GET\n
    Thursday, August 02, 2012 3:30:32 PM\n
    /webapi.hmac/api/values\n
    key1=value1&key2=value2&key3=value3
    

    请注意,表单数据和查询字符串应该是有序的,所以服务器上的代码获取查询字符串和表单数据来构建正确的消息。

    当HTTP请求到达服务器时,实现一个认证动作过滤器来解析请求以获取信息:HTTP动词、时间戳、uri、表单数据和查询字符串,然后基于这些构建签名(使用hmac哈希)使用服务器上的密钥(散列密码)。

    密钥是使用请求中的用户名从数据库中获取的。

    然后服务器代码将请求上的签名与构建的签名进行比较;如果相等,则认证通过,否则认证失败。

    构建签名的代码:

    private static string ComputeHash(string hashedPassword, string message)
    {
        var key = Encoding.UTF8.GetBytes(hashedPassword.ToUpper());
        string hashString;
    
        using (var hmac = new HMACSHA256(key))
        {
            var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
            hashString = Convert.ToBase64String(hash);
        }
    
        return hashString;
    }
    

    那么,如何防止重放攻击呢?

    为时间戳添加约束,例如:

    servertime - X minutes|seconds  <= timestamp <= servertime + X minutes|seconds 
    

    (servertime: 请求到达服务器的时间)

    并且,将请求的签名缓存在内存中(使用MemoryCache,要在时限内保存)。如果下一个请求与上一个请求具有相同的签名,它将被拒绝。

    演示代码如下: https://github.com/cuongle/Hmac.WebApi

    【讨论】:

    • @James:只有时间戳似乎不够,在短时间内他们可能会模拟请求并发送到服务器,我刚刚编辑了我的帖子,最好同时使用。
    • 你确定这能正常工作吗?您正在使用消息对时间戳进行哈希处理并缓存该消息。这意味着每个请求都有一个不同的签名,这将使您的缓存签名无用。
    • @FilipStas: 好像我没明白你的意思,这里使用缓存的原因是为了防止中继攻击,仅此而已
    • @ChrisO:您可以参考[此页面] (jokecamp.wordpress.com/2012/10/21/…)。我会尽快更新这个来源
    • 建议的解决方案有效,但您无法阻止中间人攻击,因为您必须实现 HTTPS
    【解决方案2】:

    我建议首先从最直接的解决方案开始 - 在您的场景中,简单的 HTTP 基本身份验证 + HTTPS 就足够了。

    如果没有(例如您不能使用 https,或者需要更复杂的密钥管理),您可以查看其他人建议的基于 HMAC 的解决方案。这种 API 的一个很好的例子是 Amazon S3 (http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html)

    我写了一篇关于 ASP.NET Web API 中基于 HMAC 的身份验证的博客文章。它讨论了 Web API 服务和 Web API 客户端,并且代码在 bitbucket 上可用。 http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

    这是一篇关于 Web API 中的基本身份验证的帖子:http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

    请记住,如果您要向第 3 方提供 API,那么您很可能还要负责交付客户端库。基本身份验证在这里具有显着优势,因为它在大多数开箱即用的编程平台上都受支持。另一方面,HMAC 没有那么标准化,需要自定义实现。这些应该相对简单,但仍然需要工作。

    PS。还有一个使用 HTTPS + 证书的选项。 http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

    【讨论】:

      【解决方案3】:

      您尝试过 DevDefined.OAuth 吗?

      我已经使用它来保护我的 WebApi 与 2-Legged OAuth。我也用 PHP 客户端成功测试过。

      使用这个库添加对 OAuth 的支持非常容易。以下是实现 ASP.NET MVC Web API 提供程序的方法:

      1) 获取 DevDefined.OAuth 的源代码:https://github.com/bittercoder/DevDefined.OAuth - 最新版本允许 OAuthContextBuilder 扩展。

      2) 构建库并在您的 Web API 项目中引用它。

      3) 创建自定义上下文构建器以支持从HttpRequestMessage 构建上下文:

      using System;
      using System.Collections.Generic;
      using System.Collections.Specialized;
      using System.Diagnostics.CodeAnalysis;
      using System.Linq;
      using System.Net.Http;
      using System.Web;
      
      using DevDefined.OAuth.Framework;
      
      public class WebApiOAuthContextBuilder : OAuthContextBuilder
      {
          public WebApiOAuthContextBuilder()
              : base(UriAdjuster)
          {
          }
      
          public IOAuthContext FromHttpRequest(HttpRequestMessage request)
          {
              var context = new OAuthContext
                  {
                      RawUri = this.CleanUri(request.RequestUri), 
                      Cookies = this.CollectCookies(request), 
                      Headers = ExtractHeaders(request), 
                      RequestMethod = request.Method.ToString(), 
                      QueryParameters = request.GetQueryNameValuePairs()
                          .ToNameValueCollection(), 
                  };
      
              if (request.Content != null)
              {
                  var contentResult = request.Content.ReadAsByteArrayAsync();
                  context.RawContent = contentResult.Result;
      
                  try
                  {
                      // the following line can result in a NullReferenceException
                      var contentType = 
                          request.Content.Headers.ContentType.MediaType;
                      context.RawContentType = contentType;
      
                      if (contentType.ToLower()
                          .Contains("application/x-www-form-urlencoded"))
                      {
                          var stringContentResult = request.Content
                              .ReadAsStringAsync();
                          context.FormEncodedParameters = 
                              HttpUtility.ParseQueryString(stringContentResult.Result);
                      }
                  }
                  catch (NullReferenceException)
                  {
                  }
              }
      
              this.ParseAuthorizationHeader(context.Headers, context);
      
              return context;
          }
      
          protected static NameValueCollection ExtractHeaders(
              HttpRequestMessage request)
          {
              var result = new NameValueCollection();
      
              foreach (var header in request.Headers)
              {
                  var values = header.Value.ToArray();
                  var value = string.Empty;
      
                  if (values.Length > 0)
                  {
                      value = values[0];
                  }
      
                  result.Add(header.Key, value);
              }
      
              return result;
          }
      
          protected NameValueCollection CollectCookies(
              HttpRequestMessage request)
          {
              IEnumerable<string> values;
      
              if (!request.Headers.TryGetValues("Set-Cookie", out values))
              {
                  return new NameValueCollection();
              }
      
              var header = values.FirstOrDefault();
      
              return this.CollectCookiesFromHeaderString(header);
          }
      
          /// <summary>
          /// Adjust the URI to match the RFC specification (no query string!!).
          /// </summary>
          /// <param name="uri">
          /// The original URI. 
          /// </param>
          /// <returns>
          /// The adjusted URI. 
          /// </returns>
          private static Uri UriAdjuster(Uri uri)
          {
              return
                  new Uri(
                      string.Format(
                          "{0}://{1}{2}{3}", 
                          uri.Scheme, 
                          uri.Host, 
                          uri.IsDefaultPort ?
                              string.Empty :
                              string.Format(":{0}", uri.Port), 
                          uri.AbsolutePath));
          }
      }
      

      4) 使用本教程创建 OAuth 提供者:http://code.google.com/p/devdefined-tools/wiki/OAuthProvider。在最后一步(访问受保护资源示例)中,您可以在 AuthorizationFilterAttribute 属性中使用此代码:

      public override void OnAuthorization(HttpActionContext actionContext)
      {
          // the only change I made is use the custom context builder from step 3:
          OAuthContext context = 
              new WebApiOAuthContextBuilder().FromHttpRequest(actionContext.Request);
      
          try
          {
              provider.AccessProtectedResourceRequest(context);
      
              // do nothing here
          }
          catch (OAuthException authEx)
          {
              // the OAuthException's Report property is of the type "OAuthProblemReport", it's ToString()
              // implementation is overloaded to return a problem report string as per
              // the error reporting OAuth extension: http://wiki.oauth.net/ProblemReporting
              actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
                  {
                     RequestMessage = request, ReasonPhrase = authEx.Report.ToString()
                  };
          }
      }
      

      我已经实现了我自己的提供程序,所以我没有测试上面的代码(当然除了我在我的提供程序中使用的WebApiOAuthContextBuilder),但它应该可以正常工作。

      【讨论】:

      • 谢谢 - 我会看看这个,不过现在我已经推出了自己的基于 HMAC 的解决方案。
      • @CraigShearer - 嗨,你说你已经推出了自己的..如果你不介意分享的话,只是有几个问题。我处于类似的位置,我有一个相对较小的 MVC Web API。 API 控制器与表单身份验证下的其他控制器/操作并排放置。当我已经有一个可以使用的会员提供程序并且我只需要保护少数操作时,实施 OAuth 似乎有点过头了。我真的想要一个返回加密令牌的身份验证操作 - 然后在后续调用中使用该令牌?在我承诺实施现有的身份验证解决方案之前,欢迎提供任何信息。谢谢!
      • @Maksymilian Majer - 您是否有机会更详细地分享您如何实现提供程序?我在将响应发送回客户端时遇到了一些问题。
      【解决方案4】:

      Web API 引入了属性[Authorize] 以提供安全性。这可以全局设置(global.asx)

      public static void Register(HttpConfiguration config)
      {
          config.Filters.Add(new AuthorizeAttribute());
      }
      

      或每个控制器:

      [Authorize]
      public class ValuesController : ApiController{
      ...
      

      当然,您的身份验证类型可能会有所不同,并且您可能希望执行自己的身份验证,当发生这种情况时,您可能会发现从 Authorizate Attribute 继承并对其进行扩展以满足您的要求很有用:

      public class DemoAuthorizeAttribute : AuthorizeAttribute
      {
          public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
          {
              if (Authorize(actionContext))
              {
                  return;
              }
              HandleUnauthorizedRequest(actionContext);
          }
      
          protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
          {
              var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
              challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
              throw new HttpResponseException(challengeMessage);
          }
      
          private bool Authorize(System.Web.Http.Controllers.HttpActionContext actionContext)
          {
              try
              {
                  var someCode = (from h in actionContext.Request.Headers where h.Key == "demo" select h.Value.First()).FirstOrDefault();
                  return someCode == "myCode";
              }
              catch (Exception)
              {
                  return false;
              }
          }
      }
      

      在你的控制器中:

      [DemoAuthorize]
      public class ValuesController : ApiController{
      

      这里是 WebApi 授权的其他自定义实现的链接:

      http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-membership-provider/

      【讨论】:

      • 感谢@Dalorzo 的示例,但我有一些问题。我查看了附加的链接,但遵循该说明并不太奏效。我还发现缺少所需的信息。首先,当我创建新项目时,选择个人用户帐户进行身份验证是否正确?还是我不进行身份验证。我也没有收到提到的 302 错误,但收到了 401 错误。最后,我如何将所需的信息从我的视图传递给控制器​​?我的 ajax 调用必须是什么样的?顺便说一句,我正在为我的 MVC 视图使用表单身份验证。有问题吗?
      • 它工作得非常好。很高兴学习并开始使用我们自己的访问令牌。
      • 一个小评论 - 小心AuthorizeAttribute,因为在不同的命名空间中有两个具有相同名称的不同类:1. System.Web.Mvc.AuthorizeAttribute -> 用于 MVC 控制器 2. System .Web.Http.AuthorizeAttribute -> 用于 WebApi。
      【解决方案5】:

      如果您想以服务器到服务器的方式保护您的 API(不重定向到网站进行 2 腿身份验证)。您可以查看 OAuth2 Client Credentials Grant 协议。

      https://dev.twitter.com/docs/auth/application-only-auth

      我开发了一个库,可以帮助您轻松地将这种支持添加到您的 WebAPI。您可以将其安装为 NuGet 包:

      https://nuget.org/packages/OAuth2ClientCredentialsGrant/1.0.0.0

      该库面向 .NET Framework 4.5。

      将包添加到项目后,它将在项目的根目录中创建一个自述文件。您可以查看该自述文件以了解如何配置/使用此软件包。

      干杯!

      【讨论】:

      • 您是否以开源方式共享/提供此框架的源代码?
      • JFR:第一个链接已损坏,NuGet 包从未更新
      【解决方案6】:

      继续@Cuong Le的回答,我防止重放攻击的方法是

      // 使用共享私钥(或用户密码)在客户端加密Unix时间

      // 将其作为请求标头的一部分发送到服务器(WEB API)

      // 使用共享私钥(或用户密码)解密服务器端的Unix时间(WEB API)

      //检查客户端的Unix时间和服务器的Unix时间的时间差,不能大于x秒

      //如果用户ID/哈希密码正确并且解密的UnixTime在服务器时间的x秒内,那么它是一个有效的请求

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多