После разработки веб-API, прежде чем предоставлять его своим клиентам, в зависимости от ваших потребностей, вам может потребоваться защитить некоторые или все части вашей службы API, чтобы только проверенные пользователи могли получить доступ к вашей службе API. Эта защита в ASP.NET может быть достигнута с использованием механизмов аутентификации и авторизации.
Аутентификация
Аутентификация — это процесс определения того, является ли кто-то или что-то на самом деле тем, кем или чем он является. Используя механизм аутентификации, мы гарантируем, что каждый запрос, полученный службой Web API, отправляется от клиента с надлежащими учетными данными.
Аутентификация с использованием обработчиков сообщений
Обработчик сообщений — это класс, который получает HTTP-запрос и возвращает HTTP-ответ. Обработчики сообщений являются производными от абстрактного класса HttpMessageHandler
. Они хороши для сквозных задач, которые работают на уровне HTTP-сообщений (а не действий контроллера). Например, обработчик сообщения может:
- читать или изменять заголовки запроса
- добавить заголовок ответа к ответам
- проверять запросы, прежде чем они достигнут контроллера
В веб-API, как правило, ряд обработчиков сообщений объединяется в цепочку, образуя шаблон, называемый делегирующим обработчиком.
Порядок, в котором устанавливаются эти обработчики, важен, так как они будут выполняться последовательно.
Самый важный обработчик находится на самом верху, охраняя все, что входит. Если проверки пройдены, он передаст этот запрос по цепочке следующему делегирующему обработчику и так далее.
Если все идет хорошо, он прибывает в контроллер API и выполняет желаемое действие. Однако если какая-либо из проверок завершается неудачно в обработчиках, запрос отклоняется, и клиенту отправляется ответ.
Теперь, когда у нас много теории, давайте напишем код для наших обработчиков. В этой статье мы создадим два обработчика сообщений:
- APIKeyHandler : обработчик, отвечающий за перехват HTTP-запроса и обеспечение того, чтобы его заголовок содержал ключ API
- AuthHandler : обработчик, отвечающий за аутентификацию учетных данных и ролей пользователя
Аутентификация по ключу API
В своем проекте 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 ».
Давайте APIKeyHandler
что APIKeyHandler
работает нормально, создав HTTP-запрос с правильными заголовками. Для этого нам нужно создать заголовок HTTP со значением ключа:
"API_KEY" : "X-some-key"
Я использую плагин браузера Mozilla под названием «HTTP Tool» для создания заголовков 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 происходит позже в конвейере, после
аутентификация и перед выполнением действий контроллера.
Использование атрибута Authorize
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] Атрибут
Другой способ иметь как защищенные, так и незащищенные действия в контроллере — это использовать атрибут [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 .
Я надеюсь, что вам понравилось читать столько же, сколько и извлечь уроки из этой статьи, и не забывайте оставлять любые вопросы или комментарии в ленте ниже!