Статьи

Защита ASP.NET Web API

После разработки веб-API, прежде чем предоставлять его своим клиентам, в зависимости от ваших потребностей, вам может потребоваться защитить некоторые или все части вашей службы API, чтобы только проверенные пользователи могли получить доступ к вашей службе API. Эта защита в ASP.NET может быть достигнута с использованием механизмов аутентификации и авторизации.

Аутентификация — это процесс определения того, является ли кто-то или что-то на самом деле тем, кем или чем он является. Используя механизм аутентификации, мы гарантируем, что каждый запрос, полученный службой Web API, отправляется от клиента с надлежащими учетными данными.

Обработчик сообщений — это класс, который получает HTTP-запрос и возвращает HTTP-ответ. Обработчики сообщений являются производными от абстрактного класса HttpMessageHandler . Они хороши для сквозных задач, которые работают на уровне HTTP-сообщений (а не действий контроллера). Например, обработчик сообщения может:

  • читать или изменять заголовки запроса
  • добавить заголовок ответа к ответам
  • проверять запросы, прежде чем они достигнут контроллера

В веб-API, как правило, ряд обработчиков сообщений объединяется в цепочку, образуя шаблон, называемый делегирующим обработчиком.

Поток HTTP-запросов через обработчики сообщений

Порядок, в котором устанавливаются эти обработчики, важен, так как они будут выполняться последовательно.

Самый важный обработчик находится на самом верху, охраняя все, что входит. Если проверки пройдены, он передаст этот запрос по цепочке следующему делегирующему обработчику и так далее.

Если все идет хорошо, он прибывает в контроллер API и выполняет желаемое действие. Однако если какая-либо из проверок завершается неудачно в обработчиках, запрос отклоняется, и клиенту отправляется ответ.

Теперь, когда у нас много теории, давайте напишем код для наших обработчиков. В этой статье мы создадим два обработчика сообщений:

  1. APIKeyHandler : обработчик, отвечающий за перехват HTTP-запроса и обеспечение того, чтобы его заголовок содержал ключ API
  2. AuthHandler : обработчик, отвечающий за аутентификацию учетных данных и ролей пользователя

В своем проекте Web API создайте папку с именем MessageHandlers и добавьте класс APIKeyHandler.cs .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class APIKeyHandler : DelegatingHandler
   {
       //set a default API key
       private const string yourApiKey = «X-some-key»;
 
       protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
       {
           bool isValidAPIKey = false;
           IEnumerable<string> lsHeaders;
           //Validate that the api key exists
 
           var checkApiKeyExists = request.Headers.TryGetValues(«API_KEY», out lsHeaders);
 
           if (checkApiKeyExists)
           {
               if (lsHeaders.FirstOrDefault().Equals(yourApiKey))
               {
                   isValidAPIKey = true;
               }
           }
                
           //If the key is not valid, return an http status code.
           if (!isValidAPIKey)
               return request.CreateResponse(HttpStatusCode.Forbidden, «Bad API Key»);
 
           //Allow the request to process further down the pipeline
           var response = await base.SendAsync(request, cancellationToken);
 
           //Return the response back up the chain
           return response;
       }
   }

APIKeyHandler.cs наследуется от DelegatingHandler , который, в свою очередь, наследуется от HttpMessageHandler . Это позволяет нам переопределить функциональность для проверки HTTP-запроса и контролировать, хотим ли мы разрешить этому запросу течь по конвейеру к следующему обработчику и контроллеру или остановить запрос, отправив пользовательский ответ.

В этом классе мы достигаем этого путем переопределения метода SendAsync . Этот метод ищет ключ API ( API_KEY ) в заголовке каждого HTTP-запроса и передает запрос контроллеру, только если в заголовке запроса присутствует действительный ключ API.

Теперь, чтобы увидеть этот обработчик в действии, нам нужно сначала зарегистрировать его в нашем приложении в методе Application_Start из файла Global.asax .

1
GlobalConfiguration.Configuration.MessageHandlers.Add(new APIKeyHandler());

Попробуйте вызвать любой метод, который вы показали через ваши контроллеры Web API, и вы должны увидеть «Bad API Key» в качестве ответа.

Для демонстрации в этой статье я использую тот же проект и URL-адреса, которые я создал в моей предыдущей статье « Разработка веб-API ASP.NET ».

API Key Handler демо

Давайте APIKeyHandler что APIKeyHandler работает нормально, создав HTTP-запрос с правильными заголовками. Для этого нам нужно создать заголовок HTTP со значением ключа:

"API_KEY" : "X-some-key"

Я использую плагин браузера Mozilla под названием «HTTP Tool» для создания заголовков HTTP-запросов.

Заголовок HTTP с помощью инструмента HTTP

HTTP-запрос теперь полностью передается контроллеру обработчиком.

Таким образом, наш обработчик проверки ключа API теперь на месте. Это защищает наш веб-API, чтобы обеспечить доступ к этой службе только тем клиентам, которым предоставлены действительные ключи API. Далее мы рассмотрим, как мы можем реализовать безопасность на основе ролей пользователей.

Базовая аутентификация, как следует из ее названия, является самой простой и базовой формой аутентификации HTTP-запросов. Клиент отправляет учетные данные в кодировке Base64 в заголовке Authorize при каждом HTTP-запросе, и только в случае проверки учетных данных API возвращает ожидаемый ответ. Обычная аутентификация не требует хранения сеансов на стороне сервера или реализации файлов cookie, поскольку каждый запрос проверяется API.

Как только базовая реализация аутентификации в Web API будет понята, будет очень легко подключить другие формы аутентификации. Только процесс аутентификации будет другим, и перехватчики веб-API, где это делается, будут одинаковыми.

Для проверки учетных данных пользователя мы создаем объект IPrincipal который представляет текущий контекст безопасности.

Добавьте в нее новую папку с именем Security и новый класс TestAPIPrincipal.cs .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestAPIPrincipal : IPrincipal
   {
       //Constructor
       public TestAPIPrincipal(string userName)
       {
           UserName = userName;
           Identity = new GenericIdentity(userName);
       }
 
       public string UserName { get;
       public IIdentity Identity { get;
       public bool IsInRole(string role)
       {
           if (role.Equals(«user»))
           {
               return true;
           }
           else
           {
               return false;
           }
       }
   }

Объект IIdentity связанный с принципалом, имеет свойство IsAuthenticated . Если пользователь аутентифицирован, это свойство вернет true; в противном случае он вернет false.

Теперь давайте создадим еще один обработчик с именем AuthHandler.cs .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class AuthHandler : DelegatingHandler
   {
       string _userName = «»;
        
       //Method to validate credentials from Authorization
       //header value
       private bool ValidateCredentials(AuthenticationHeaderValue authenticationHeaderVal)
       {
           try
           {
               if (authenticationHeaderVal != null
                   && !String.IsNullOrEmpty(authenticationHeaderVal.Parameter))
               {
                   string[] decodedCredentials
                   = Encoding.ASCII.GetString(Convert.FromBase64String(
                   authenticationHeaderVal.Parameter))
                   .Split(new[] { ‘:’ });
 
                   //now decodedCredentials[0] will contain
                   //username and decodedCredentials[1] will
                   //contain password.
 
                   if (decodedCredentials[0].Equals(«username»)
                   && decodedCredentials[1].Equals(«password»))
                   {
                       _userName = «John Doe»;
                       return true;//request authenticated.
                   }
               }
               return false;//request not authenticated.
           }
           catch {
               return false;
           }
       }
 
       protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
       {
           //if the credentials are validated,
           //set CurrentPrincipal and Current.User
           if (ValidateCredentials(request.Headers.Authorization))
           {
               Thread.CurrentPrincipal = new TestAPIPrincipal(_userName);
               HttpContext.Current.User = new TestAPIPrincipal(_userName);
           }
           //Execute base.SendAsync to execute default
           //actions and once it is completed,
           //capture the response object and add
           //WWW-Authenticate header if the request
           //was marked as unauthorized.
 
           //Allow the request to process further down the pipeline
           var response = await base.SendAsync(request, cancellationToken);
 
           if (response.StatusCode == HttpStatusCode.Unauthorized
               && !response.Headers.Contains(«WwwAuthenticate»))
           {
               response.Headers.Add(«WwwAuthenticate», «Basic»);
           }
 
           return response;
       }
   }

Этот класс содержит закрытый метод ValidateCredentials , который проверяет декодированные значения имени пользователя и пароля из заголовка HTTP-запроса, а также SendAsync метод для перехвата HTTP-запроса.

Если учетные данные клиента действительны, то текущий IPrincipal Объект присоединен к текущему потоку, т.е. Thread.CurrentPrincipal . Мы также установили HttpContext.Current.User для обеспечения согласованности контекста безопасности. Это позволяет нам получить доступ к данным текущего пользователя из любой точки приложения.

Как только запрос аутентифицирован, base.SendAsync для отправки запроса внутреннему обработчику. Если ответ содержит неавторизованный заголовок HTTP, код внедряет заголовок WwwAuthenticate со значением Basic сообщить клиенту, что наш сервис ожидает базовую аутентификацию.

Теперь нам нужно зарегистрировать этот обработчик в классе Global.Asax как мы это делали для нашего ApiKeyHandler . Убедитесь, что AuthHandler Обработчик находится ниже первой регистрации обработчика, чтобы убедиться в правильном порядке.

1
2
GlobalConfiguration.Configuration.MessageHandlers.Add(new APIKeyHandler());
GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthHandler());

Но прежде чем мы сможем увидеть базовую аутентификацию в действии, нам сначала нужно реализовать авторизацию.

Авторизация проверяет, может ли аутентифицированный пользователь выполнить конкретное действие или использовать определенный ресурс. Этот процесс в веб-API происходит позже в конвейере, после
аутентификация и перед выполнением действий контроллера.

ASP.NET MVC Web API предоставляет фильтр авторизации под названием AuthorizeAttribute который проверяет IPrincipal , проверяет его свойство Identity.IsAuthenticated и возвращает 401 Unauthorized статус 401 Unauthorized HTTP, если значение равно false и запрошенный метод действия не будет выполнен. Этот фильтр может применяться на разных уровнях, таких как уровень контроллера или уровень действия, и может быть легко применен с помощью [Authorize] синтаксис поверх контроллеров или действий.

1
2
[Authorize]
public class ClassifiedsController : ApiController

Как только этот атрибут установлен, он предотвратит доступ всех методов действия в контроллере неавторизованным пользователям.

Сначала IPrincipal наш базовый обработчик аутентификации, чтобы установить IPrincipal объект текущего пользователя. Затем, прежде чем этот запрос достигнет контроллера, AuthorizeAttribute проверяет доступ к конкретному контроллеру / действию для текущего пользователя.

Чтобы увидеть это в действии, сначала давайте создадим HTTP-запрос без надлежащих учетных данных.

Доступ запрещен для неавторизованных пользователей

Доступ запрещен AuthorizeAttribute .

Теперь давайте создадим еще один запрос с ключом / значением заголовка авторизации на этот раз следующим образом:

Авторизация: Basic dXNlcm5hbWU6cGFzc3dvcmQ =

Здесь значение dXNlcm5hbWU6cGFzc3dvcmQ= это   Base64-кодированная форма имени username:password .

Этот запрос получает права доступа к контроллеру / действию, как и ожидалось.

Доступ предоставлен пользователю с надлежащими учетными данными

Это пример защиты общедоступных действий всего контроллера.

Мы также можем ограничить некоторые части действий контроллера, установив Атрибут [Authorize] на уровне действия. Это позволит нам иметь как защищенные, так и незащищенные действия на одном контроллере.

01
02
03
04
05
06
07
08
09
10
11
12
13
//[Authorize]
   public class ClassifiedsController : ApiController
   {
       public List<ClassifiedModel> Get(string id)
       {
           return ClassifiedService.GetClassifieds(id);
       }
 
       [Authorize]
       public List<ClassifiedModel> Get()
       {
           return ClassifiedService.GetClassifieds(«»);
       }

Другой способ иметь как защищенные, так и незащищенные действия в контроллере — это использовать атрибут [AllowAnonymous] . Когда мы устанавливаем атрибут [Authorize] на уровне контроллера и устанавливаем [AllowAnonymous] для любого действия внутри контроллера, это действие пропустит атрибут [Authorize] .

Также возможно фильтровать определенные роли и пользователей по правам доступа. Например, у нас может быть что-то вроде [Authorize(Roles = "Admin")] на контроллерах и действиях.

Наконец, мы также можем создать наш собственный атрибут авторизации в зависимости от наших потребностей. Одним из способов достижения этого является расширение AuthorizeAttribute .

Скажем, мы хотим ограничить нашу службу веб-API только определенными частями мира, ограничивая доступ пользователей, которые не находятся в определенном диапазоне IP-адресов. Для этой цели мы можем создать собственный атрибут авторизации, производный от AuthorizeAttribute класс и переопределение метода IsAuthorized .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class RestrictIPsAttribute: System.Web.Http.AuthorizeAttribute
   {
       protected override bool IsAuthorized(HttpActionContext context)
       {
           var ip = HttpContext.Current.Request.UserHostAddress;
 
           //check for ip here
           if (ip.Contains(«»))
           {
               return true;
           }
           return false;
       }
   }

Получив наш собственный атрибут Authorize, мы можем украсить его контроллерами / действиями.

1
2
3
4
5
[RestrictIPsAttribute]
public List<ClassifiedModel> Get()
{
    return ClassifiedService.GetClassifieds(«»);
}

В этой статье мы рассмотрели, как мы можем обезопасить нашу службу веб-API ASP.NET, прежде чем раскрывать ее для внешнего мира. Мы рассмотрели, как мы можем аутентифицировать HTTP-запросы для действительных ключей API и для действительных учетных данных пользователя. Имея в своем распоряжении много знаний, я считаю, что мы готовы разработать любую пользовательскую защиту для наших API.

Для тех из вас, кто только начинает работать с Laravel или хочет расширить свои знания, сайт или приложение с помощью расширений, у нас есть множество вещей, которые вы можете изучить на Envato Market .

Я надеюсь, что вам понравилось читать столько же, сколько и извлечь уроки из этой статьи, и не забывайте оставлять любые вопросы или комментарии в ленте ниже!