Поделиться через


Статья об обеспечении безопасности веб-интерфейсов API с помощью отдельной учетной записи и локального имени входа в веб-API ASP.NET 2.2

Майк Уосон

Скачивание примера приложения

В этом разделе показано, как защитить веб-API с помощью OAuth2 для проверки подлинности в базе данных членства.

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

В Visual Studio 2013 шаблон проекта веб-API предоставляет три варианта проверки подлинности:

  • Индивидуальные учетные записи. Приложение использует базу данных членства.
  • Учетные записи организации. Пользователи входят с помощью учетных данных Azure Active Directory, Office 365 или локальной службы Active Directory.
  • Проверка подлинности Windows. Этот параметр предназначен для приложений интрасети и использует модуль IIS проверки подлинности Windows.

Дополнительные сведения об этих параметрах см. в статье Создание веб-проектов ASP.NET в Visual Studio 2013.

Отдельные учетные записи предоставляют пользователю два способа входа:

  • Локальный вход. Пользователь регистрируется на сайте, вводя имя пользователя и пароль. Приложение сохраняет хэш паролей в базе данных членства. Когда пользователь входит в систему, система удостоверений ASP.NET проверяет пароль.
  • Социальный вход. Пользователь входит во внешнюю службу, например Facebook, Майкрософт или Google. Приложение по-прежнему создает запись для пользователя в базе данных членства, но не сохраняет учетные данные. Пользователь проходит проверку подлинности, войдя во внешнюю службу.

В этой статье рассматривается сценарий локального входа. Для локального входа и входа в социальные сети веб-API использует OAuth2 для проверки подлинности запросов. Однако потоки учетных данных различаются для локального входа и входа в социальные сети.

В этой статье мы продемонстрируем простое приложение, которое позволяет пользователю входить в систему и отправлять вызовы AJAX, прошедшие проверку подлинности, в веб-API. Пример кода можно скачать здесь. Файл сведений описывает, как создать пример с нуля в Visual Studio.

Изображение примера формы

Пример приложения использует Knockout.js для привязки данных и jQuery для отправки запросов AJAX. Я сосредоточусь на вызовах AJAX, так что вам не нужно знать, Knockout.js для этой статьи.

Попутно я опишу:

  • Что делает приложение на стороне клиента.
  • Что происходит на сервере.
  • Трафик HTTP в середине.

Сначала необходимо определить некоторую терминологию OAuth2.

  • Ресурс. Некоторые данные, которые можно защитить.
  • Сервер ресурсов. Сервер, на котором размещен ресурс.
  • Владелец ресурса. Сущность, которая может предоставить разрешение на доступ к ресурсу. (Обычно это пользователь.)
  • Клиент: приложение, которому требуется доступ к ресурсу. В этой статье клиентом является веб-браузер.
  • Маркер доступа. Маркер, предоставляющий доступ к ресурсу.
  • Маркер носителя. Определенный тип маркера доступа со свойством, которое любой пользователь может использовать маркер. Иными словами, клиенту не требуется криптографический ключ или другой секрет для использования маркера носителя. По этой причине маркеры носителя должны использоваться только по протоколу HTTPS и должны иметь относительно короткие сроки действия.
  • Сервер авторизации. Сервер, который выдает маркеры доступа.

Приложение может выступать как сервер авторизации, так и сервер ресурсов. Шаблон проекта веб-API соответствует этому шаблону.

Поток учетных данных для локального входа

Для локального входа веб-API использует поток пароля владельца ресурса , определенный в OAuth2.

  1. Пользователь вводит имя и пароль в клиент.
  2. Клиент отправляет эти учетные данные на сервер авторизации.
  3. Сервер авторизации проверяет подлинность учетных данных и возвращает маркер доступа.
  4. Для доступа к защищенному ресурсу клиент включает маркер доступа в заголовок авторизации HTTP-запроса.

Схема потока локальных учетных данных для входа

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

Схема выбора отдельных учетных записей в веб-интерфейсе API

В этом сценарии контроллеры веб-API выступают в качестве серверов ресурсов. Фильтр проверки подлинности проверяет маркеры доступа, а атрибут [Authorize] используется для защиты ресурса. Если контроллер или действие имеет атрибут [Authorize] , все запросы к контроллеру или действию должны проходить проверку подлинности. В противном случае авторизация отклоняется, а веб-API возвращает ошибку 401 (не авторизовано).

Сервер авторизации и фильтр проверки подлинности вызывают компонент ПО промежуточного слоя OWIN , который обрабатывает сведения об OAuth2. Я подробно охарактеризую структуру далее в этом руководстве.

Отправка несанкционированного запроса

Чтобы приступить к работе, запустите приложение и нажмите кнопку Вызов API . Когда запрос завершится, в поле Результат появится сообщение об ошибке. Это связано с тем, что запрос не содержит маркер доступа, поэтому запрос не авторизован.

Изображение сообщения об ошибке результата

Кнопка Вызова API отправляет запрос AJAX в ~/api/values, который вызывает действие контроллера веб-API. Ниже приведен раздел кода JavaScript, который отправляет запрос AJAX. В примере приложения весь код приложения JavaScript находится в файле Scripts\app.js.

// If we already have a bearer token, set the Authorization header.
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
    headers.Authorization = 'Bearer ' + token;
}

$.ajax({
    type: 'GET',
    url: 'api/values/1',
    headers: headers
}).done(function (data) {
    self.result(data);
}).fail(showError);

До входа пользователя в запрос не будет маркера носителя и, следовательно, заголовка авторизации. Это приводит к тому, что запрос возвращает ошибку 401.

Ниже приведен HTTP-запрос. (Я использовал Fiddler для записи трафика HTTP.)

GET https://localhost:44305/api/values HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/

HTTP-ответ:

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer
Date: Tue, 30 Sep 2014 21:54:43 GMT
Content-Length: 61

{"Message":"Authorization has been denied for this request."}

Обратите внимание, что ответ содержит заголовок Www-Authenticate с заданием Bearer. Это означает, что сервер ожидает маркер носителя.

Регистрация пользователя

В разделе Регистрация приложения введите адрес электронной почты и пароль и нажмите кнопку Зарегистрировать .

Вам не нужно использовать допустимый адрес электронной почты для этого примера, но реальное приложение подтвердит его. (См. статью Создание защищенного веб-приложения ASP.NET MVC 5 с помощью входа, подтверждения электронной почты и сброса пароля.) Введите пароль, например "Password1!", с прописными буквами, строчными буквами, цифрами и символами, не являющиеся буквами. Чтобы сделать приложение простым, я пропустил проверку на стороне клиента, поэтому при возникновении проблем с форматом пароля вы получите ошибку 400 (недопустимый запрос).

Изображение раздела регистрации пользователя

Кнопка Зарегистрировать отправляет запрос POST в ~/api/Account/Register/. Текст запроса — это объект JSON, содержащий имя и пароль. Ниже приведен код JavaScript, который отправляет запрос:

var data = {
    Email: self.registerEmail(),
    Password: self.registerPassword(),
    ConfirmPassword: self.registerPassword2()
};

$.ajax({
    type: 'POST',
    url: '/api/Account/Register',
    contentType: 'application/json; charset=utf-8',
    data: JSON.stringify(data)
}).done(function (data) {
    self.result("Done!");
}).fail(showError);

HTTP-запрос, где $CREDENTIAL_PLACEHOLDER$ — заполнитель для пары "ключ-значение" пароля:

POST https://localhost:44305/api/Account/Register HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 84

{"Email":"alice@example.com",$CREDENTIAL_PLACEHOLDER1$,$CREDENTIAL_PLACEHOLDER2$"}

HTTP-ответ:

HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0

Этот запрос обрабатывается классом AccountController . AccountController Внутренне использует ASP.NET Identity для управления базой данных членства.

Если вы запускаете приложение локально из Visual Studio, учетные записи пользователей хранятся в LocalDB в таблице AspNetUsers. Чтобы просмотреть таблицы в Visual Studio, откройте меню Вид, выберите Сервер Обозреватель, а затем разверните узел Подключения к данным.

Изображение подключений к данным

Получение маркера доступа

До сих пор мы не сделали OAuth, но теперь мы увидим сервер авторизации OAuth в действии при запросе маркера доступа. В области Вход примера приложения введите адрес электронной почты и пароль и нажмите кнопку Войти.

Изображение раздела входа

Кнопка Войти отправляет запрос к конечной точке маркера. Текст запроса содержит следующие данные в кодировке form-url:

  • grant_type: "password"
  • username: <адрес электронной почты пользователя.>
  • password: <password>

Ниже приведен код JavaScript, который отправляет запрос AJAX:

var loginData = {
    grant_type: 'password',
    username: self.loginEmail(),
    password: self.loginPassword()
};

$.ajax({
    type: 'POST',
    url: '/Token',
    data: loginData
}).done(function (data) {
    self.user(data.userName);
    // Cache the access token in session storage.
    sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);

Если запрос выполнен успешно, сервер авторизации возвращает маркер доступа в тексте ответа. Обратите внимание, что маркер хранится в хранилище сеансов, чтобы использовать его позже при отправке запросов к API. В отличие от некоторых форм проверки подлинности (например, проверки подлинности на основе файлов cookie), браузер не будет автоматически включать маркер доступа в последующие запросы. Приложение должно сделать это явным образом. Это хорошо, потому что это ограничивает уязвимости CSRF.

HTTP-запрос:

POST https://localhost:44305/Token HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 68

grant_type=password&username=alice%40example.com&password=Password1!

Вы увидите, что запрос содержит учетные данные пользователя. Для обеспечения безопасности транспортного уровня необходимо использовать ПРОТОКОЛ HTTPS.

HTTP-ответ:

HTTP/1.1 200 OK
Content-Length: 669
Content-Type: application/json;charset=UTF-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:22:36 GMT

{
  "access_token":"imSXTs2OqSrGWzsFQhIXziFCO3rF...",
  "token_type":"bearer",
  "expires_in":1209599,
  "userName":"alice@example.com",
  ".issued":"Wed, 01 Oct 2014 01:22:33 GMT",
  ".expires":"Wed, 15 Oct 2014 01:22:33 GMT"
}

Для удобочитаемости я отступил JSON и усекал маркер доступа, который является довольно длинным.

Свойства access_token, token_typeи expires_in определяются спецификацией OAuth2. Другие свойства (userName, .issuedи .expires) предназначены только для информационных целей. Код, добавляющий эти дополнительные свойства, можно найти в методе TokenEndpoint в файле /Providers/ApplicationOAuthProvider.cs.

Отправка запроса, прошедшего проверку подлинности

Теперь, когда у нас есть маркер носителя, можно выполнить запрос к API с проверкой подлинности. Для этого задается заголовок Authorization в запросе. Нажмите кнопку Вызов API еще раз, чтобы увидеть это.

Изображение после нажатия кнопки A IP-вызова

HTTP-запрос:

GET https://localhost:44305/api/values/1 HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Authorization: Bearer imSXTs2OqSrGWzsFQhIXziFCO3rF...
X-Requested-With: XMLHttpRequest

HTTP-ответ:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:41:29 GMT
Content-Length: 27

"Hello, alice@example.com."

Выход

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

self.logout = function () {
    sessionStorage.removeItem(tokenKey)
}

Общие сведения о шаблоне проекта "Отдельные учетные записи"

При выборе отдельных учетных записей в шаблоне проекта веб-приложения ASP.NET проект включает:

  • Сервер авторизации OAuth2.
  • Конечная точка веб-API для управления учетными записями пользователей
  • Модель EF для хранения учетных записей пользователей.

Ниже приведены main классы приложений, реализующие эти функции:

  • AccountController. Предоставляет конечную точку веб-API для управления учетными записями пользователей. Это единственное Register действие, которое мы использовали в этом руководстве. Другие методы в классе поддерживают сброс пароля, входы в социальные сети и другие функции.
  • ApplicationUser, определенный в файле /Models/IdentityModels.cs. Этот класс является моделью EF для учетных записей пользователей в базе данных членства.
  • ApplicationUserManager, определенный в файле /App_Start/IdentityConfig.cs. Этот класс является производным от UserManager и выполняет операции с учетными записями пользователей, такие как создание нового пользователя, проверка паролей и т. д., и автоматически сохраняет изменения в базе данных.
  • ApplicationOAuthProvider. Этот объект подключается к ПО промежуточного слоя OWIN и обрабатывает события, вызванные ПО промежуточного слоя. Он является производным от OAuthAuthorizationServerProvider.

Изображение классов приложений main

Настройка сервера авторизации

В файле StartupAuth.cs следующий код настраивает сервер авторизации OAuth2.

PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
    // Note: Remove the following line before you deploy to production:
    AllowInsecureHttp = true
};

// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);

Свойство TokenEndpointPath — это URL-путь к конечной точке сервера авторизации. Это URL-адрес, который приложение использует для получения маркеров носителя.

Свойство Provider указывает поставщика, который подключается к ПО промежуточного слоя OWIN и обрабатывает события, вызванные ПО промежуточного слоя.

Ниже приведен базовый поток, когда приложение хочет получить маркер:

  1. Чтобы получить маркер доступа, приложение отправляет запрос в ~/Token.
  2. ПО промежуточного слоя OAuth вызывает GrantResourceOwnerCredentials в поставщике.
  3. Поставщик вызывает для ApplicationUserManager проверки учетных данных и создания удостоверения утверждений.
  4. В случае успешного выполнения поставщик создает билет проверки подлинности, который используется для создания маркера.

Схема потока авторизации

ПО промежуточного слоя OAuth ничего не знает об учетных записях пользователей. Поставщик взаимодействует между ПО промежуточного слоя и ASP.NET Identity. Дополнительные сведения о реализации сервера авторизации см. в разделе OWIN OAuth 2.0 Authorization Server.

Настройка веб-API для использования маркеров носителя

В методе WebApiConfig.Register следующий код настраивает проверку подлинности для конвейера веб-API:

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

Класс HostAuthenticationFilter обеспечивает проверку подлинности с помощью маркеров носителя.

Метод SuppressDefaultHostAuthentication предписывает веб-API игнорировать любую проверку подлинности, которая происходит до того, как запрос достигнет конвейера веб-API, с помощью IIS или ПО промежуточного слоя OWIN. Таким образом можно ограничить веб-API для проверки подлинности только с помощью токенов носителя.

Примечание

В частности, часть приложения MVC может использовать проверку подлинности с помощью форм, при которой учетные данные хранятся в файле cookie. Для проверки подлинности на основе файлов cookie требуется использование маркеров защиты от подделки для предотвращения атак CSRF. Это проблема для веб-API, так как веб-API не может отправить клиенту маркер защиты от подделки. (Дополнительные сведения об этой проблеме см. в разделе Предотвращение атак CSRF в веб-API.) Вызов SuppressDefaultHostAuthentication гарантирует, что веб-API не будет уязвим к атакам CSRF с учетных данных, хранящихся в файлах cookie.

Когда клиент запрашивает защищенный ресурс, в конвейере веб-API происходит вот что:

  1. Фильтр HostAuthentication вызывает ПО промежуточного слоя OAuth для проверки маркера.
  2. ПО промежуточного слоя преобразует маркер в удостоверение утверждений.
  3. На этом этапе запрос проходит проверку подлинности, но не авторизован.
  4. Фильтр авторизации проверяет удостоверение утверждений. Если утверждения авторизуют пользователя для этого ресурса, запрос авторизуется. По умолчанию атрибут [Authorize] будет авторизовать любой запрос, прошедший проверку подлинности. Однако вы можете авторизоваться с помощью роли или других утверждений. Дополнительные сведения см. в статье Проверка подлинности и авторизация в веб-API.
  5. Если предыдущие шаги выполнены успешно, контроллер возвращает защищенный ресурс. В противном случае клиент получает ошибку 401 (не авторизовано).

Схема того, когда клиент запрашивает защищенный ресурс

Дополнительные ресурсы