Cách bảo mật API Web ASP.NET [đã đóng]


397

Tôi muốn xây dựng một dịch vụ web RESTful bằng API Web ASP.NET mà các nhà phát triển bên thứ ba sẽ sử dụng để truy cập dữ liệu của ứng dụng của tôi.

Tôi đã đọc khá nhiều về OAuth và nó có vẻ là tiêu chuẩn, nhưng việc tìm một mẫu tốt với tài liệu giải thích cách thức hoạt động của nó (và điều đó thực sự hoạt động!) Dường như vô cùng khó khăn (đặc biệt đối với người mới sử dụng OAuth).

Có một mẫu thực sự xây dựng và hoạt động và cho thấy làm thế nào để thực hiện điều này?

Tôi đã tải xuống nhiều mẫu:

  • DotNetOAuth - tài liệu là vô vọng từ góc độ người mới
  • Thinktecture - không thể lấy nó để xây dựng

Tôi cũng đã xem các blog đề xuất một sơ đồ dựa trên mã thông báo đơn giản (như thế này ) - điều này có vẻ giống như phát minh lại bánh xe nhưng nó có lợi thế là về mặt khái niệm khá đơn giản.

Có vẻ như có nhiều câu hỏi như thế này trên SO nhưng không có câu trả lời hay.

Mọi người đang làm gì trong không gian này?

Câu trả lời:


292

Cập nhật:

Tôi đã thêm liên kết này vào câu trả lời khác của mình về cách sử dụng xác thực JWT cho API Web ASP.NET tại đây cho bất kỳ ai quan tâm đến JWT.


Chúng tôi đã quản lý để áp dụng xác thực HMAC cho API Web bảo mật và nó hoạt động tốt. Xác thực HMAC sử dụng một khóa bí mật cho mỗi người tiêu dùng mà cả người tiêu dùng và máy chủ đều biết để hmac băm một tin nhắn, nên sử dụng HMAC256. Hầu hết các trường hợp, mật khẩu băm của người tiêu dùng được sử dụng như một khóa bí mật.

Thông báo thường được xây dựng từ dữ liệu trong yêu cầu HTTP hoặc thậm chí dữ liệu tùy chỉnh được thêm vào tiêu đề HTTP, thông báo có thể bao gồm:

  1. Dấu thời gian: thời gian yêu cầu được gửi (UTC hoặc GMT)
  2. Động từ HTTP: GET, POST, PUT, DELETE.
  3. gửi dữ liệu và chuỗi truy vấn,
  4. URL

Dưới vỏ bọc, xác thực HMAC sẽ là:

Người tiêu dùng gửi yêu cầu HTTP đến máy chủ web, sau khi xây dựng chữ ký (đầu ra của hàm băm hmac), mẫu yêu cầu HTTP:

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

Ví dụ cho yêu cầu GET:

GET /webapi.hmac/api/values

User-Agent: Fiddler    
Host: localhost    
Timestamp: Thursday, August 02, 2012 3:30:32 PM 
Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=

Thông báo băm để lấy chữ ký:

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

Ví dụ cho yêu cầu POST với chuỗi truy vấn (chữ ký bên dưới không chính xác, chỉ là một ví dụ)

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

Thông báo băm để có được chữ ký

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

Xin lưu ý rằng dữ liệu biểu mẫu và chuỗi truy vấn phải theo thứ tự, vì vậy mã trên máy chủ nhận chuỗi truy vấn và dữ liệu biểu mẫu để tạo thông báo chính xác.

Khi yêu cầu HTTP đến máy chủ, bộ lọc hành động xác thực được triển khai để phân tích yêu cầu để lấy thông tin: Động từ HTTP, dấu thời gian, uri, dữ liệu biểu mẫu và chuỗi truy vấn, sau đó dựa trên những điều này để xây dựng chữ ký (sử dụng hàm băm hmac) với bí mật khóa (mật khẩu băm) trên máy chủ.

Khóa bí mật được lấy từ cơ sở dữ liệu với tên người dùng theo yêu cầu.

Sau đó, mã máy chủ so sánh chữ ký theo yêu cầu với chữ ký được xây dựng; nếu bằng nhau, xác thực được thông qua, nếu không, nó đã thất bại.

Mã để xây dựng chữ ký:

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;
}

Vậy, làm thế nào để ngăn chặn tấn công lại?

Thêm ràng buộc cho dấu thời gian, đại loại như:

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

(servertime: thời gian yêu cầu đến máy chủ)

Và, lưu trữ chữ ký của yêu cầu trong bộ nhớ (sử dụng MemoryCache, nên giữ trong giới hạn thời gian). Nếu yêu cầu tiếp theo đi kèm cùng chữ ký với yêu cầu trước đó, nó sẽ bị từ chối.

Mã trình diễn được đặt ở đây: https://github.com/cuongle/Hmac.WebApi


2
@James: chỉ có dấu thời gian dường như không đủ, trong thời gian ngắn họ có thể mô phỏng yêu cầu và gửi đến máy chủ, tôi vừa chỉnh sửa bài đăng của mình, sử dụng cả hai sẽ là tốt nhất.
cuongle

1
Bạn có chắc chắn điều này đang làm việc như nó nên? bạn đang băm dấu thời gian với thông báo và lưu trữ thông báo đó. Điều này có nghĩa là một chữ ký khác nhau cho mỗi yêu cầu sẽ làm cho chữ ký được lưu trong bộ nhớ cache của bạn trở nên vô dụng.
Filip Stas

1
@FilipStas: có vẻ như tôi không hiểu ý của bạn, lý do để sử dụng Cache ở đây là để ngăn chặn cuộc tấn công tiếp sức, không có gì hơn
cuongle

1
@ChrisO: Bạn có thể tham khảo [trang này] ( jokecamp.wordpress.com/2012/10/21/iêu ). Tôi sẽ cập nhật nguồn này sớm
cuongle

1
Giải pháp đề xuất có hiệu quả, nhưng bạn không thể ngăn chặn cuộc tấn công Man-in-the-Middle, vì bạn phải thực hiện HTTPS
tái cấu trúc

34

Tôi sẽ đề nghị bắt đầu với các giải pháp đơn giản nhất trước tiên - có thể đơn giản xác thực cơ bản HTTP + HTTPS là đủ trong kịch bản của bạn.

Nếu không (ví dụ: bạn không thể sử dụng https hoặc cần quản lý khóa phức tạp hơn), bạn có thể xem xét các giải pháp dựa trên HMAC theo đề xuất của người khác. Một ví dụ điển hình về API như vậy sẽ là Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

Tôi đã viết một bài đăng trên blog về xác thực dựa trên HMAC trong API Web ASP.NET. Nó thảo luận về cả dịch vụ API Web và ứng dụng API Web và mã có sẵn trên bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Đây là một bài viết về Xác thực cơ bản trong API Web: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-USE-message-handlers/

Hãy nhớ rằng nếu bạn định cung cấp API cho bên thứ 3, rất có thể bạn sẽ chịu trách nhiệm phân phối thư viện khách. Xác thực cơ bản có một lợi thế đáng kể ở đây vì nó được hỗ trợ trên hầu hết các nền tảng lập trình. Mặt khác, HMAC không được chuẩn hóa và sẽ yêu cầu thực hiện tùy chỉnh. Những điều này nên tương đối đơn giản nhưng vẫn đòi hỏi phải làm việc.

Tái bút Ngoài ra còn có một tùy chọn để sử dụng chứng chỉ HTTPS +. http://www.piotrwalat.net/client-certert-authentication-in-asp-net-web-api-and-windows-store-apps/


23

Bạn đã thử DevDefined.OAuth chưa?

Tôi đã sử dụng nó để bảo mật WebApi của mình với OAuth 2 chân. Tôi cũng đã thử nghiệm thành công với các máy khách PHP.

Thật dễ dàng để thêm hỗ trợ cho OAuth bằng thư viện này. Đây là cách bạn có thể triển khai nhà cung cấp cho API Web ASP.NET:

1) Nhận mã nguồn của DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - phiên bản mới nhất cho phép OAuthContextBuildermở rộng.

2) Xây dựng thư viện và tham chiếu nó trong dự án API Web của bạn.

3) Tạo trình tạo bối cảnh tùy chỉnh để hỗ trợ xây dựng bối cảnh từ 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) Sử dụng hướng dẫn này để tạo nhà cung cấp OAuth: http://code.google.com.vn/p/devdposed-tools/wiki/OAuthProvider . Trong bước cuối cùng (Truy cập ví dụ tài nguyên được bảo vệ), bạn có thể sử dụng mã này trong AuthorizationFilterAttributethuộc tính của mình :

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()
            };
    }
}

Tôi đã triển khai nhà cung cấp riêng của mình vì vậy tôi đã không kiểm tra mã trên (tất nhiên ngoại trừ mã WebApiOAuthContextBuildertôi đang sử dụng trong nhà cung cấp của mình) nhưng nó sẽ hoạt động tốt.


Cảm ơn - Tôi sẽ xem xét điều này, mặc dù hiện tại tôi đã triển khai giải pháp dựa trên HMAC của riêng mình.
Craig Shearer

1
@CraigShearer - xin chào, bạn nói rằng bạn đã tự lăn .. chỉ cần có một vài câu hỏi nếu bạn không ngại chia sẻ. Tôi đang ở một vị trí tương tự, nơi tôi có API Web MVC tương đối nhỏ. Các bộ điều khiển API nằm bên cạnh các bộ điều khiển / hành động khác dưới biểu mẫu auth. Việc triển khai OAuth có vẻ quá mức khi tôi đã có một nhà cung cấp thành viên mà tôi có thể sử dụng và tôi chỉ cần bảo đảm một số ít hoạt động. Tôi thực sự muốn một hành động xác thực trả về mã thông báo được mã hóa - sau đó sử dụng mã thông báo trong các cuộc gọi tiếp theo? mọi thông tin đều được chào đón trước khi tôi cam kết thực hiện giải pháp xác thực hiện có. cảm ơn!
sambomartin

@Maksymilian Majer - Bất kỳ cơ hội nào bạn có thể chia sẻ cách bạn đã triển khai nhà cung cấp chi tiết hơn? Tôi đang gặp một số vấn đề khi gửi phản hồi cho khách hàng.
jlrolin

21

API Web đã giới thiệu một Thuộc tính [Authorize]để cung cấp bảo mật. Điều này có thể được đặt trên toàn cầu (global.asx)

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

Hoặc trên mỗi bộ điều khiển:

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

Tất nhiên loại xác thực của bạn có thể thay đổi và bạn có thể muốn thực hiện xác thực của riêng mình, khi điều này xảy ra, bạn có thể thấy kế thừa hữu ích từ Thuộc tính ủy quyền và mở rộng nó để đáp ứng yêu cầu của bạn:

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;
        }
    }
}

Và trong bộ điều khiển của bạn:

[DemoAuthorize]
public class ValuesController : ApiController{

Đây là một liên kết về việc triển khai tùy chỉnh khác cho Ủy quyền WebApi:

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


Cảm ơn ví dụ @Dalorzo, nhưng tôi có một số vấn đề. Tôi đã xem liên kết đính kèm, nhưng làm theo hướng dẫn đó không hoàn toàn hiệu quả. Tôi cũng tìm thấy thông tin cần thiết bị thiếu. Thứ nhất, khi tôi tạo dự án mới, có đúng không khi chọn Tài khoản người dùng cá nhân để xác thực? Hoặc tôi để nó không có xác thực. Tôi cũng không nhận được lỗi 302 được đề cập, nhưng tôi đang gặp lỗi 401. Cuối cùng, làm cách nào để chuyển thông tin cần thiết từ chế độ xem của tôi sang bộ điều khiển? Cuộc gọi ajax của tôi phải như thế nào? Btw, tôi đang sử dụng xác thực mẫu cho các khung nhìn MVC của tôi. Đó có phải là vấn đề không?
Amanda

Nó đang làm việc tuyệt vời. Chỉ cần tốt đẹp để tìm hiểu và bắt đầu làm việc trên các mã thông báo truy cập của riêng chúng tôi.
CodeName47

Một nhận xét nhỏ - hãy cẩn thận AuthorizeAttribute, vì có hai lớp khác nhau có cùng tên, trong các không gian tên khác nhau: 1. System.Web.Mvc.AuthorizeAttribution -> cho bộ điều khiển MVC 2. System.Web.Http.AuthorizeAttribution -> cho WebApi.
Vitaliy Markitanov

5

Nếu bạn muốn bảo mật API của mình trong máy chủ sang máy chủ thời trang (không chuyển hướng đến trang web để xác thực 2 chân). Bạn có thể xem giao thức Cấp chứng chỉ ứng dụng khách OAuth2.

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

Tôi đã phát triển một thư viện có thể giúp bạn dễ dàng thêm loại hỗ trợ này vào WebAPI của bạn. Bạn có thể cài đặt nó dưới dạng gói NuGet:

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

Thư viện nhắm mục tiêu .NET Framework 4.5.

Khi bạn thêm gói vào dự án của bạn, nó sẽ tạo một tệp readme trong thư mục gốc của dự án. Bạn có thể nhìn vào tệp readme đó để xem cách cấu hình / sử dụng gói này.

Chúc mừng!


5
Bạn đang chia sẻ / cung cấp mã nguồn cho khung này dưới dạng nguồn mở?
barrypicker

JFR: Liên kết đầu tiên bị hỏng và gói NuGet chưa bao giờ được cập nhật
abdul qayyum

3

tiếp tục câu trả lời của @ Cường Lê, cách tiếp cận của tôi để ngăn chặn cuộc tấn công lại sẽ là

// Mã hóa Thời gian Unix ở phía Máy khách bằng khóa riêng được chia sẻ (hoặc mật khẩu của người dùng)

// Gửi nó như một phần của tiêu đề yêu cầu đến máy chủ (API WEB)

// Giải mã thời gian Unix tại máy chủ (API WEB) bằng khóa riêng được chia sẻ (hoặc mật khẩu của người dùng)

// Kiểm tra chênh lệch thời gian giữa Thời gian Unix của Máy khách và Thời gian Unix của Máy chủ, không được lớn hơn x giây

// nếu ID người dùng / Mật khẩu băm là chính xác và UnixTime được giải mã trong vòng x giây của máy chủ thì đó là một yêu cầu hợp lệ

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.