404

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

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

是否有实际构建和工作的示例并显示如何实现它?

我已经下载了许多示例:

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

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

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

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

4

6 回答 6

299

更新:

对于任何对 JWT 感兴趣的人,我已将此链接添加到我的其他答案中,如何在此处为 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 哈希的输出)后,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

于 2012-08-02T17:02:43.377 回答
34

我建议首先从最直接的解决方案开始 - 在您的场景中,简单的 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/

于 2013-03-01T12:11:26.010 回答
23

你试过 DevDefined.OAuth 吗?

我已经使用它通过 2-Legged OAuth 来保护我的 WebApi。我还成功地使用 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我在我的提供程序中使用的代码),但它应该可以正常工作。

于 2012-08-13T07:25:28.423 回答
22

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/

于 2014-02-07T18:04:00.097 回答
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。

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

干杯!

于 2013-05-20T03:47:39.013 回答
4

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

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

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

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

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

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

于 2015-10-16T11:36:21.280 回答