如何保护ASP.NET Web API [关闭]


397

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

我已经阅读了很多有关OAuth的文章,这似乎是标准的,但是要找到一个很好的示例来说明其工作原理(实际上是有效的!)的文档似乎非常困难(特别是对于使用OAuth的新手)。

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

我下载了许多示例:

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

我也看过博客,提出了一个基于令牌的简单方案(像这样)-好像是在重新发明轮子,但是这样做的确在概念上相当简单。

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

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

Answers:


292

更新:

我已将此链接添加到我的其他答案中,如何在此为对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身份验证将是:

消费者在构建签名(hmac哈希的输出)后,将HTTP请求发送到Web服务器,这是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


2
@James:仅时间戳似乎还不够,在短时间内他们可能会模拟请求并将其发送到服务器,我刚刚编辑了我的帖子,最好同时使用两者。
cuongle 2012年

1
您确定这可以正常工作吗?您正在使用消息对时间戳进行哈希处理并对该消息进行缓存。这意味着每个请求都有不同的签名,这会使您的缓存签名无用。
Filip Stas

1
@FilipStas:似乎我不明白您的意思,在这里使用Cache的原因是为了防止中继攻击,仅此而已
cuongle

1
@ChrisO:您可以参考[本页](jokecamp.wordpress.com/2012/10/21/…)。我会尽快更新这个源
cuongle

1
该解决方案建议有效,但您不能阻止中间人攻击,因为您必须实施HTTPS
重构

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/

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

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


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,我在提供程序中使用的除外 ),但是它应该可以正常工作。


谢谢-我将看一看,尽管目前我已经推出了自己的基于HMAC的解决方案。
Craig Shearer

1
@CraigShearer-嗨,您说您已经自己滚动了..如果您不介意共享,只会有几个问题。我处于类似的位置,那里有一个相对较小的MVC Web API。API控制器与其他auth / form形式的控制器/动作并排放置。当我已经有可以使用的成员资格提供程序并且仅需要保护少数几个操作时,实施OAuth似乎是过大的选择。我真的想要一个返回加密令牌的身份验证操作-然后在后续调用中使用该令牌吗?在我承诺实施现有身份验证解决方案之前,欢迎您提供任何信息。谢谢!
sambomartin 2012年

@Maksymilian Majer-是否有机会分享您更详细地实施提供程序的方式?我在将响应发送回客户端时遇到一些问题。
jlrolin 2014年

21

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视图使用表单身份验证。那是问题吗?
阿曼达

它的运行异常出色。很高兴学习并开始使用我们自己的访问令牌。
CodeName47

一个小注释-请注意AuthorizeAttribute,因为在不同的命名空间中有两个具有相同名称的不同类:1. System.Web.Mvc.AuthorizeAttribute->用于MVC控制器2. System.Web.Http.AuthorizeAttribute->用于WebApi。
Vitaliy Markitanov

5

如果您想以服务器到服务器的方式保护API(两腿身份验证无需重定向到网站)。您可以查看OAuth2客户端凭据授予协议。

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

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

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

该库面向.NET Framework 4.5。

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

干杯!


5
您是否以开放源代码的形式共享/提供该框架的源代码?
barrypicker 2014年

JFR:First Link已损坏,NuGet软件包从未更新过
Abdul qayyum

3

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

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

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

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

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

//如果用户ID /哈希密码正确,并且解密的UnixTime在服务器时间的x秒以内,则为有效请求

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.