Udostępnij za pośrednictwem


Zabezpieczanie internetowego interfejsu API przy użyciu indywidualnych kont i logowania lokalnego w interfejsie API sieci Web 2.2 ASP.NET

Autor: Mike Wasson

Pobierz przykładową aplikację

W tym temacie przedstawiono sposób zabezpieczania internetowego interfejsu API przy użyciu protokołu OAuth2 w celu uwierzytelniania w bazie danych członkostwa.

Wersje oprogramowania używane w samouczku

W programie Visual Studio 2013 szablon projektu internetowego interfejsu API udostępnia trzy opcje uwierzytelniania:

  • Indywidualne konta. Aplikacja używa bazy danych członkostwa.
  • Konta organizacyjne. Użytkownicy logują się przy użyciu swoich poświadczeń usługi Azure Active Directory, office 365 lub lokalnych poświadczeń usługi Active Directory.
  • Uwierzytelnianie systemu Windows. Ta opcja jest przeznaczona dla aplikacji intranetowych i używa modułu Usługi IIS uwierzytelniania systemu Windows.

Poszczególne konta zapewniają dwa sposoby logowania użytkownika:

  • Logowanie lokalne. Użytkownik rejestruje się w witrynie, wprowadzając nazwę użytkownika i hasło. Aplikacja przechowuje skrót haseł w bazie danych członkostwa. Gdy użytkownik się zaloguje, system ASP.NET Identity weryfikuje hasło.
  • Logowanie społecznościowe. Użytkownik loguje się przy użyciu usługi zewnętrznej, takiej jak Facebook, Microsoft lub Google. Aplikacja nadal tworzy wpis dla użytkownika w bazie danych członkostwa, ale nie przechowuje żadnych poświadczeń. Użytkownik uwierzytelnia się, logując się do usługi zewnętrznej.

W tym artykule przedstawiono scenariusz logowania lokalnego. W przypadku logowania lokalnego i społecznościowego internetowy interfejs API używa protokołu OAuth2 do uwierzytelniania żądań. Jednak przepływy poświadczeń różnią się w przypadku logowania lokalnego i społecznościowego.

W tym artykule przedstawię prostą aplikację, która umożliwia użytkownikowi logowanie się i wysyłanie uwierzytelnionych wywołań AJAX do internetowego interfejsu API. Przykładowy kod można pobrać tutaj. W pliku readme opisano sposób tworzenia przykładu od podstaw w programie Visual Studio.

Obraz przykładowego formularza

Przykładowa aplikacja używa Knockout.js do tworzenia powiązań danych i zapytania jQuery do wysyłania żądań AJAX. Skupię się na wywołaniach AJAX, więc nie musisz wiedzieć, Knockout.js dla tego artykułu.

Po drodze opiszę:

  • Co robi aplikacja po stronie klienta.
  • Co się dzieje na serwerze.
  • Ruch HTTP w środku.

Najpierw musimy zdefiniować terminologię OAuth2.

  • Zasób. Niektóre dane, które mogą być chronione.
  • Serwer zasobów. Serwer hostujący zasób.
  • Właściciel zasobu. Jednostka, która może udzielić uprawnień dostępu do zasobu. (Zazwyczaj użytkownik).
  • Klient: aplikacja, która chce uzyskać dostęp do zasobu. W tym artykule klient jest przeglądarką internetową.
  • Token dostępu. Token, który udziela dostępu do zasobu.
  • Token elementu nośnego. Określony typ tokenu dostępu z właściwością, którą każdy może użyć tokenu. Innymi słowy, klient nie potrzebuje klucza kryptograficznego ani innego wpisu tajnego, aby użyć tokenu elementu nośnego. Z tego powodu tokeny elementu nośnego powinny być używane tylko za pośrednictwem protokołu HTTPS i powinny mieć stosunkowo krótkie czasy wygaśnięcia.
  • Serwer autoryzacji. Serwer, który udostępnia tokeny dostępu.

Aplikacja może działać zarówno jako serwer autoryzacji, jak i serwer zasobów. Szablon projektu internetowego interfejsu API jest zgodny z tym wzorcem.

Przepływ poświadczeń logowania lokalnego

W przypadku logowania lokalnego internetowy interfejs API używa przepływu hasła właściciela zasobu zdefiniowanego w usłudze OAuth2.

  1. Użytkownik wprowadza nazwę i hasło do klienta.
  2. Klient wysyła te poświadczenia do serwera autoryzacji.
  3. Serwer autoryzacji uwierzytelnia poświadczenia i zwraca token dostępu.
  4. Aby uzyskać dostęp do chronionego zasobu, klient zawiera token dostępu w nagłówku Autoryzacja żądania HTTP.

Diagram przepływu poświadczeń logowania lokalnego

Po wybraniu pozycji Indywidualne konta w szablonie projektu internetowego interfejsu API projekt zawiera serwer autoryzacji, który weryfikuje poświadczenia użytkownika i wystawia tokeny. Na poniższym diagramie przedstawiono ten sam przepływ poświadczeń pod względem składników internetowego interfejsu API.

Diagram po wybraniu poszczególnych kont w sieci Web A I

W tym scenariuszu kontrolery internetowego interfejsu API działają jako serwery zasobów. Filtr uwierzytelniania weryfikuje tokeny dostępu, a atrybut [Autoryzuj] jest używany do ochrony zasobu. Jeśli kontroler lub akcja ma atrybut [Autoryzuj], wszystkie żądania do tego kontrolera lub akcji muszą być uwierzytelnione. W przeciwnym razie autoryzacja zostanie odrzucona, a internetowy interfejs API zwróci błąd 401 (Brak autoryzacji).

Serwer autoryzacji i uwierzytelnianie filtrują oba wywołania do składnika oprogramowania pośredniczącego OWIN, który obsługuje szczegóły protokołu OAuth2. W dalszej części tego samouczka opiszę projekt.

Wysyłanie nieautoryzowanego żądania

Aby rozpocząć, uruchom aplikację i kliknij przycisk Wywołaj interfejs API . Po zakończeniu żądania w polu Wynik powinien zostać wyświetlony komunikat o błędzie. Dzieje się tak, ponieważ żądanie nie zawiera tokenu dostępu, więc żądanie nie jest autoryzowane.

Obraz komunikatu o błędzie wyniku

Przycisk Wywołaj interfejs API wysyła żądanie AJAX do ~/api/values, które wywołuje akcję kontrolera internetowego interfejsu API. Oto sekcja kodu JavaScript, która wysyła żądanie AJAX. W przykładowej aplikacji cały kod aplikacji JavaScript znajduje się w pliku 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);

Dopóki użytkownik nie zaloguje się, nie ma tokenu elementu nośnego i dlatego nie ma nagłówka autoryzacji w żądaniu. Powoduje to zwrócenie błędu 401 przez żądanie.

Oto żądanie HTTP. (Użyto Program Fiddler do przechwytywania ruchu 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/

Odpowiedź 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."}

Zwróć uwagę, że odpowiedź zawiera nagłówek Www-Authenticate z wyzwaniem ustawionym na bearer. Oznacza to, że serwer oczekuje tokenu elementu nośnego.

Rejestrowanie użytkownika

W sekcji Rejestrowanie aplikacji wprowadź adres e-mail i hasło, a następnie kliknij przycisk Zarejestruj.

Nie musisz używać prawidłowego adresu e-mail dla tego przykładu, ale prawdziwa aplikacja potwierdzi ten adres. (Zobacz Utwórz bezpieczną ASP.NET aplikację internetową MVC 5 z logowaniem, potwierdzeniem wiadomości e-mail i resetowaniem hasła). W przypadku hasła użyj znaku "Password1!", z wielkim literą, małą literą, cyfrą i znakiem nienumerycznym. Aby zachować prostą aplikację, pominąłem weryfikację po stronie klienta, więc jeśli wystąpi problem z formatem hasła, zostanie wyświetlony błąd 400 (nieprawidłowe żądanie).

Obraz przedstawiający sekcję rejestrowanie użytkownika

Przycisk Zarejestruj wysyła żądanie POST do ~/api/Account/Register/. Treść żądania jest obiektem JSON, który zawiera nazwę i hasło. Oto kod JavaScript, który wysyła żądanie:

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

Żądanie HTTP, gdzie $CREDENTIAL_PLACEHOLDER$ jest symbolem zastępczym pary klucz-wartość hasła:

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$"}

Odpowiedź HTTP:

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

To żądanie jest obsługiwane przez klasę AccountController . AccountController Wewnętrznie używa ASP.NET Identity do zarządzania bazą danych członkostwa.

Jeśli aplikacja jest uruchamiana lokalnie z poziomu programu Visual Studio, konta użytkowników są przechowywane w bazie danych LocalDB w tabeli AspNetUsers. Aby wyświetlić tabele w programie Visual Studio, kliknij menu Widok , wybierz pozycję Eksplorator serwera, a następnie rozwiń węzeł Połączenia danych.

Obraz połączeń danych

Uzyskiwanie tokenu dostępu

Do tej pory nie zrobiliśmy żadnego protokołu OAuth, ale teraz zobaczymy serwer autoryzacji OAuth w działaniu, gdy zażądamy tokenu dostępu. W obszarze Zaloguj w przykładowej aplikacji wprowadź adres e-mail i hasło, a następnie kliknij pozycję Zaloguj.

Obraz przedstawiający sekcję logowania

Przycisk Zaloguj wysyła żądanie do punktu końcowego tokenu. Treść żądania zawiera następujące dane zakodowane w postaci adresu URL:

  • grant_type: "hasło"
  • nazwa użytkownika: <adres e-mail użytkownika>
  • hasło: <hasło>

Oto kod JavaScript, który wysyła żądanie 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);

Jeśli żądanie zakończy się pomyślnie, serwer autoryzacji zwróci token dostępu w treści odpowiedzi. Zwróć uwagę, że token jest przechowywany w magazynie sesji, który będzie używany później podczas wysyłania żądań do interfejsu API. W przeciwieństwie do niektórych form uwierzytelniania (takich jak uwierzytelnianie oparte na plikach cookie), przeglądarka nie będzie automatycznie dołączać tokenu dostępu do kolejnych żądań. Aplikacja musi to zrobić jawnie. To dobra rzecz, ponieważ ogranicza luki w zabezpieczeniach CSRF.

Żądanie 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!

Widać, że żądanie zawiera poświadczenia użytkownika. Aby zapewnić zabezpieczenia warstwy transportu, należy użyć protokołu HTTPS.

Odpowiedź 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"
}

Aby uzyskać czytelność, wcięto kod JSON i obcięto token dostępu, który jest dość długi.

Właściwości access_token, token_typei expires_in są definiowane przez specyfikację OAuth2. Inne właściwości (userName, .issued, i .expires) są przeznaczone tylko do celów informacyjnych. Kod, który dodaje te dodatkowe właściwości w metodzie TokenEndpoint , można znaleźć w pliku /Providers/ApplicationOAuthProvider.cs.

Wysyłanie uwierzytelnionego żądania

Teraz, gdy mamy token elementu nośnego, możemy wysłać uwierzytelnione żądanie do interfejsu API. W tym celu należy ustawić nagłówek autoryzacji w żądaniu. Kliknij ponownie przycisk Wywołaj interfejs API , aby to zobaczyć.

Obraz po wywołaniu przycisku P I został kliknięty

Żądanie 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

Odpowiedź 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."

Wylogowywanie

Ponieważ przeglądarka nie buforuje poświadczeń ani tokenu dostępu, wylogowanie się jest po prostu kwestią "zapominania" tokenu przez usunięcie go z magazynu sesji:

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

Opis szablonu projektu Indywidualne konta

Po wybraniu pozycji Indywidualne konta w szablonie projektu aplikacji internetowej ASP.NET projekt obejmuje następujące elementy:

  • Serwer autoryzacji OAuth2.
  • Punkt końcowy internetowego interfejsu API do zarządzania kontami użytkowników
  • Model EF do przechowywania kont użytkowników.

Oto główne klasy aplikacji, które implementują te funkcje:

  • AccountController. Udostępnia internetowy punkt końcowy interfejsu API do zarządzania kontami użytkowników. Akcja Register jest jedyną używaną w tym samouczku. Inne metody w klasie obsługują resetowanie haseł, logowania społecznościowe i inne funkcje.
  • ApplicationUser, zdefiniowany w /Models/IdentityModels.cs. Ta klasa jest modelem EF dla kont użytkowników w bazie danych członkostwa.
  • ApplicationUserManager, zdefiniowany w /App_Start/IdentityConfig.cs Ta klasa pochodzi z UserManager i wykonuje operacje na kontach użytkowników, takich jak tworzenie nowego użytkownika, weryfikowanie haseł itd., i automatycznie utrwala zmiany w bazie danych.
  • ApplicationOAuthProvider. Ten obiekt podłącza oprogramowanie pośredniczące OWIN i przetwarza zdarzenia zgłaszane przez oprogramowanie pośredniczące. Pochodzi on z OAuthAuthorizationServerProvider.

Obraz głównych klas aplikacji

Konfigurowanie serwera autoryzacji

W StartupAuth.cs poniższy kod konfiguruje serwer autoryzacji 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);

Właściwość TokenEndpointPath jest ścieżką adresu URL do punktu końcowego serwera autoryzacji. Jest to adres URL używany przez aplikację do pobierania tokenów elementu nośnego.

Właściwość Provider określa dostawcę, który podłącza oprogramowanie pośredniczące OWIN i przetwarza zdarzenia zgłaszane przez oprogramowanie pośredniczące.

Oto podstawowy przepływ, gdy aplikacja chce uzyskać token:

  1. Aby uzyskać token dostępu, aplikacja wysyła żądanie do ~/Token.
  2. Oprogramowanie pośredniczące OAuth wywołuje GrantResourceOwnerCredentials dostawcę.
  3. Dostawca wywołuje metodę ApplicationUserManager , aby zweryfikować poświadczenia i utworzyć tożsamość oświadczeń.
  4. Jeśli to się powiedzie, dostawca utworzy bilet uwierzytelniania, który jest używany do generowania tokenu.

Diagram przepływu autoryzacji

Oprogramowanie pośredniczące OAuth nie wie nic o kontach użytkowników. Dostawca komunikuje się między oprogramowaniem pośredniczącym a ASP.NET Identity. Aby uzyskać więcej informacji na temat implementowania serwera autoryzacji, zobacz OWIN OAuth 2.0 Authorization Server(Serwer autoryzacji OWIN 2.0).

Konfigurowanie internetowego interfejsu API do używania tokenów elementu nośnego

W metodzie WebApiConfig.Register następujący kod konfiguruje uwierzytelnianie dla potoku internetowego interfejsu API:

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

Klasa HostAuthenticationFilter umożliwia uwierzytelnianie przy użyciu tokenów elementu nośnego.

Metoda SuppressDefaultHostAuthentication informuje internetowy interfejs API o ignorowaniu uwierzytelniania, które ma miejsce przed dotarciem żądania do potoku internetowego interfejsu API przez usługi IIS lub oprogramowanie pośredniczące OWIN. Dzięki temu możemy ograniczyć internetowy interfejs API do uwierzytelniania tylko przy użyciu tokenów elementu nośnego.

Uwaga

W szczególności część MVC aplikacji może używać uwierzytelniania formularzy, które przechowuje poświadczenia w pliku cookie. Uwierzytelnianie oparte na plikach cookie wymaga użycia tokenów przeciw fałszerzowaniu, aby zapobiec atakom CSRF. Jest to problem z internetowymi interfejsami API, ponieważ nie ma wygodnego sposobu wysyłania tokenu ochrony przed fałszerstwa do klienta. (Aby uzyskać więcej informacji na temat tego problemu, zobacz Zapobieganie atakom CSRF w internetowym interfejsie API). Wywołanie metody SuppressDefaultHostAuthentication gwarantuje, że internetowy interfejs API nie jest narażony na ataki CSRF z poświadczeń przechowywanych w plikach cookie.

Gdy klient żąda chronionego zasobu, oto co się dzieje w potoku internetowego interfejsu API:

  1. Filtr HostAuthentication wywołuje oprogramowanie pośredniczące OAuth w celu zweryfikowania tokenu.
  2. Oprogramowanie pośredniczące konwertuje token na tożsamość oświadczeń.
  3. W tym momencie żądanie jest uwierzytelniane , ale nie jest autoryzowane.
  4. Filtr autoryzacji sprawdza tożsamość oświadczeń. Jeśli oświadczenia autoryzuje użytkownika dla tego zasobu, żądanie jest autoryzowane. Domyślnie atrybut [Autoryzuj] autoryzuje każde uwierzytelnione żądanie. Można jednak autoryzować przez rolę lub inne oświadczenia. Aby uzyskać więcej informacji, zobacz Uwierzytelnianie i autoryzacja w internetowym interfejsie API.
  5. Jeśli poprzednie kroki zostaną wykonane pomyślnie, kontroler zwróci chroniony zasób. W przeciwnym razie klient otrzymuje błąd 401 (Brak autoryzacji).

Diagram przedstawiający, kiedy klient żąda chronionego zasobu

Dodatkowe zasoby