Filtry uwierzytelniania w interfejsie ASP.NET Web API 2
Autor: Mike Wasson
Filtr uwierzytelniania to składnik, który uwierzytelnia żądanie HTTP. Interfejs API sieci Web 2 i MVC 5 obsługują filtry uwierzytelniania, ale różnią się nieznacznie, głównie w konwencjach nazewnictwa dla interfejsu filtru. W tym temacie opisano filtry uwierzytelniania interfejsu API sieci Web.
Filtry uwierzytelniania umożliwiają ustawienie schematu uwierzytelniania dla poszczególnych kontrolerów lub akcji. Dzięki temu aplikacja może obsługiwać różne mechanizmy uwierzytelniania dla różnych zasobów HTTP.
W tym artykule pokażę kod z przykładowego uwierzytelniania podstawowego w witrynie https://github.com/aspnet/samples. W przykładzie przedstawiono filtr uwierzytelniania, który implementuje schemat uwierzytelniania podstawowego dostępu HTTP (RFC 2617). Filtr jest implementowany w klasie o nazwie IdentityBasicAuthenticationAttribute
. Nie pokażę całego kodu z przykładu— tylko części ilustrujące sposób pisania filtru uwierzytelniania.
Ustawianie filtru uwierzytelniania
Podobnie jak w przypadku innych filtrów, filtry uwierzytelniania można stosować dla poszczególnych kontrolerów, poszczególnych akcji lub globalnie do wszystkich kontrolerów interfejsu API sieci Web.
Aby zastosować filtr uwierzytelniania do kontrolera, udekoruj klasę kontrolera za pomocą atrybutu filter. Poniższy kod ustawia [IdentityBasicAuthentication]
filtr w klasie kontrolera, co umożliwia uwierzytelnianie podstawowe dla wszystkich akcji kontrolera.
[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
public IHttpActionResult Post() { . . . }
}
Aby zastosować filtr do jednej akcji, udekoruj akcję filtrem. Poniższy kod ustawia [IdentityBasicAuthentication]
filtr metody kontrolera Post
.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
[IdentityBasicAuthentication] // Enable Basic authentication for this action.
public IHttpActionResult Post() { . . . }
}
Aby zastosować filtr do wszystkich kontrolerów internetowego interfejsu API, dodaj go do pliku GlobalConfiguration.Filters.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new IdentityBasicAuthenticationAttribute());
// Other configuration code not shown...
}
}
Implementowanie filtru uwierzytelniania internetowego interfejsu API
W internetowym interfejsie API filtry uwierzytelniania implementują interfejs System.Web.Http.Filters.IAuthenticationFilter . Powinny one również dziedziczyć z System.Attribute, aby można je było stosować jako atrybuty.
Interfejs IAuthenticationFilter ma dwie metody:
- Element AuthenticateAsync uwierzytelnia żądanie, sprawdzając poświadczenia w żądaniu, jeśli istnieje.
- ChallengeAsync dodaje żądanie uwierzytelnienia do odpowiedzi HTTP, jeśli jest to konieczne.
Metody te odpowiadają przepływowi uwierzytelniania zdefiniowanemu w dokumencie RFC 2612 i RFC 2617:
- Klient wysyła poświadczenia w nagłówku Autoryzacja. Zwykle dzieje się tak po otrzymaniu przez klienta odpowiedzi 401 (Brak autoryzacji) z serwera. Jednak klient może wysyłać poświadczenia z dowolnym żądaniem, a nie tylko po otrzymaniu 401.
- Jeśli serwer nie akceptuje poświadczeń, zwraca odpowiedź 401 (Brak autoryzacji). Odpowiedź zawiera nagłówek Www-Authenticate zawierający co najmniej jedno wyzwanie. Każde wyzwanie określa schemat uwierzytelniania rozpoznawany przez serwer.
Serwer może również zwrócić błąd 401 z żądania anonimowego. W rzeczywistości zazwyczaj jest to sposób inicjowania procesu uwierzytelniania:
- Klient wysyła żądanie anonimowe.
- Serwer zwraca wartość 401.
- Klienci ponownie wysyła żądanie przy użyciu poświadczeń.
Ten przepływ obejmuje zarówno kroki uwierzytelniania, jak i autoryzacji .
- Uwierzytelnianie potwierdza tożsamość klienta.
- Autoryzacja określa, czy klient może uzyskać dostęp do określonego zasobu.
W internetowym interfejsie API filtry uwierzytelniania obsługują uwierzytelnianie, ale nie autoryzację. Autoryzacja powinna być wykonywana przez filtr autoryzacji lub wewnątrz akcji kontrolera.
Oto przepływ w potoku internetowego interfejsu API 2:
- Przed wywołaniem akcji internetowy interfejs API tworzy listę filtrów uwierzytelniania dla tej akcji. Obejmuje to filtry z zakresem akcji, zakresem kontrolera i zakresem globalnym.
- Internetowy interfejs API wywołuje metodę AuthenticateAsync na każdym filtrze na liście. Każdy filtr może zweryfikować poświadczenia w żądaniu. Jeśli dowolny filtr pomyślnie weryfikuje poświadczenia, filtr tworzy element IPrincipal i dołącza go do żądania. W tym momencie filtr może również wyzwolić błąd. Jeśli tak, pozostała część potoku nie zostanie uruchomiona.
- Zakładając, że nie ma błędu, żądanie przepływa przez pozostałą część potoku.
- Na koniec internetowy interfejs API wywołuje metodę ChallengeAsync każdego filtru uwierzytelniania. Filtry używają tej metody, aby w razie potrzeby dodać wyzwanie do odpowiedzi. Zazwyczaj (ale nie zawsze), które wystąpiłyby w odpowiedzi na błąd 401.
Na poniższych diagramach przedstawiono dwa możliwe przypadki. W pierwszym filtrze uwierzytelniania pomyślnie uwierzytelnia żądanie, filtr autoryzacji autoryzuje żądanie, a akcja kontrolera zwraca wartość 200 (OK).
W drugim przykładzie filtr uwierzytelniania uwierzytelnia żądanie, ale filtr autoryzacji zwraca wartość 401 (Brak autoryzacji). W takim przypadku akcja kontrolera nie jest wywoływana. Filtr uwierzytelniania dodaje nagłówek Www-Authenticate do odpowiedzi.
Możliwe są inne kombinacje — na przykład jeśli akcja kontrolera zezwala na żądania anonimowe, może istnieć filtr uwierzytelniania, ale bez autoryzacji.
Implementowanie metody AuthenticateAsync
Metoda AuthenticateAsync próbuje uwierzytelnić żądanie. W tym miejscu znajduje się sygnatura metody:
Task AuthenticateAsync(
HttpAuthenticationContext context,
CancellationToken cancellationToken
)
Metoda AuthenticateAsync musi wykonać jedną z następujących czynności:
- Nic (no-op).
- Utwórz obiekt IPrincipal i ustaw go na żądanie.
- Ustaw wynik błędu.
Opcja (1) oznacza, że żądanie nie ma żadnych poświadczeń, które rozumie filtr. Opcja (2) oznacza, że filtr pomyślnie uwierzytelnił żądanie. Opcja (3) oznacza, że żądanie miało nieprawidłowe poświadczenia (takie jak nieprawidłowe hasło), co wyzwala odpowiedź z błędem.
Poniżej przedstawiono ogólny konspekt implementowania funkcji AuthenticateAsync.
- Poszukaj poświadczeń w żądaniu.
- Jeśli nie ma żadnych poświadczeń, nie rób nic i zwracaj (no-op).
- Jeśli istnieją poświadczenia, ale filtr nie rozpoznaje schematu uwierzytelniania, nie rób nic i zwracaj (no-op). Inny filtr w potoku może zrozumieć schemat.
- Jeśli istnieją poświadczenia zrozumiałe dla filtru, spróbuj je uwierzytelnić.
- Jeśli poświadczenia są nieprawidłowe, zwróć wartość 401, ustawiając wartość
context.ErrorResult
. - Jeśli poświadczenia są prawidłowe, utwórz element IPrincipal i ustaw wartość
context.Principal
.
Poniższy kod przedstawia metodę AuthenticationAsync z przykładowego uwierzytelniania podstawowego . Komentarze wskazują każdy krok. Kod przedstawia kilka typów błędów: nagłówek autoryzacji bez poświadczeń, źle sformułowane poświadczenia i nieprawidłowa nazwa użytkownika/hasło.
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// 2. If there are no credentials, do nothing.
if (authorization == null)
{
return;
}
// 3. If there are credentials but the filter does not recognize the
// authentication scheme, do nothing.
if (authorization.Scheme != "Basic")
{
return;
}
// 4. If there are credentials that the filter understands, try to validate them.
// 5. If the credentials are bad, set the error result.
if (String.IsNullOrEmpty(authorization.Parameter))
{
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return;
}
Tuple<string, string> userNameAndPassword = ExtractUserNameAndPassword(authorization.Parameter);
if (userNameAndPassword == null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
}
string userName = userNameAndPassword.Item1;
string password = userNameAndPassword.Item2;
IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
if (principal == null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
// 6. If the credentials are valid, set principal.
else
{
context.Principal = principal;
}
}
Ustawianie wyniku błędu
Jeśli poświadczenia są nieprawidłowe, filtr musi mieć ustawioną context.ErrorResult
wartość IHttpActionResult , która tworzy odpowiedź o błędzie. Aby uzyskać więcej informacji na temat interfejsu IHttpActionResult, zobacz Wyniki akcji w internetowym interfejsie API 2.
Przykład uwierzytelniania podstawowego zawiera klasę AuthenticationFailureResult
, która jest odpowiednia do tego celu.
public class AuthenticationFailureResult : IHttpActionResult
{
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
ReasonPhrase = reasonPhrase;
Request = request;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}
}
Implementowanie narzędzia ChallengeAsync
Celem metody ChallengeAsync jest dodanie do odpowiedzi wyzwań związanych z uwierzytelnianiem, jeśli jest to konieczne. W tym miejscu znajduje się sygnatura metody:
Task ChallengeAsync(
HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken
)
Metoda jest wywoływana dla każdego filtru uwierzytelniania w potoku żądania.
Należy pamiętać, że narzędzie ChallengeAsync jest wywoływane przed utworzeniem odpowiedzi HTTP, a nawet przed uruchomieniem akcji kontrolera. Gdy wywołana jest funkcja ChallengeAsync , context.Result
zawiera element IHttpActionResult, który jest używany później do tworzenia odpowiedzi HTTP. Dlatego po wywołaniu metody ChallengeAsync nie wiesz jeszcze nic o odpowiedzi HTTP. Metoda ChallengeAsync powinna zastąpić oryginalną wartość elementu nową wartością context.Result
IHttpActionResult. Ten element IHttpActionResult musi opakowować oryginalny element context.Result
.
Wywołam oryginalny wynikIHttpActionResult i nowy wynik IHttpActionResultzewnętrzny. Wynik zewnętrzny musi wykonać następujące czynności:
- Wywołaj wewnętrzny wynik, aby utworzyć odpowiedź HTTP.
- Sprawdź odpowiedź.
- W razie potrzeby dodaj wyzwanie uwierzytelniania do odpowiedzi.
Poniższy przykład jest pobierany z przykładu uwierzytelniania podstawowego. Definiuje element IHttpActionResult dla zewnętrznego wyniku.
public class AddChallengeOnUnauthorizedResult : IHttpActionResult
{
public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult)
{
Challenge = challenge;
InnerResult = innerResult;
}
public AuthenticationHeaderValue Challenge { get; private set; }
public IHttpActionResult InnerResult { get; private set; }
public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = await InnerResult.ExecuteAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// Only add one challenge per authentication scheme.
if (!response.Headers.WwwAuthenticate.Any((h) => h.Scheme == Challenge.Scheme))
{
response.Headers.WwwAuthenticate.Add(Challenge);
}
}
return response;
}
}
Właściwość InnerResult
przechowuje wewnętrzny element IHttpActionResult. Właściwość Challenge
reprezentuje nagłówek Www-Authentication. Zwróć uwagę, że funkcja ExecuteAsync najpierw wywołuje metodę InnerResult.ExecuteAsync
, aby utworzyć odpowiedź HTTP, a następnie w razie potrzeby dodaje wyzwanie.
Przed dodaniem wyzwania sprawdź kod odpowiedzi. Większość schematów uwierzytelniania dodaje wyzwanie tylko wtedy, gdy odpowiedź to 401, jak pokazano tutaj. Jednak niektóre schematy uwierzytelniania dodają wyzwanie do odpowiedzi na sukces. Na przykład zobacz Negocjuj (RFC 4559).
Biorąc pod uwagę klasę, rzeczywisty kod w narzędziu AddChallengeOnUnauthorizedResult
ChallengeAsync jest prosty. Wystarczy utworzyć wynik i dołączyć go do context.Result
elementu .
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
var challenge = new AuthenticationHeaderValue("Basic");
context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result);
return Task.FromResult(0);
}
Uwaga: Przykład uwierzytelniania podstawowego tworzy abstrakcję tej logiki nieco, umieszczając ją w metodzie rozszerzenia.
Łączenie filtrów uwierzytelniania z uwierzytelnianiem Host-Level
"Uwierzytelnianie na poziomie hosta" to uwierzytelnianie wykonywane przez hosta (na przykład usługi IIS), zanim żądanie osiągnie strukturę internetowego interfejsu API.
Często warto włączyć uwierzytelnianie na poziomie hosta dla pozostałej części aplikacji, ale wyłączyć je dla kontrolerów internetowego interfejsu API. Na przykład typowym scenariuszem jest włączenie uwierzytelniania formularzy na poziomie hosta, ale użycie uwierzytelniania opartego na tokenach dla internetowego interfejsu API.
Aby wyłączyć uwierzytelnianie na poziomie hosta wewnątrz potoku internetowego interfejsu API, wywołaj metodę config.SuppressHostPrincipal()
w konfiguracji. Powoduje to, że internetowy interfejs API usuwa element IPrincipal z dowolnego żądania, które wchodzi do potoku internetowego interfejsu API. Skutecznie "wyrejektywuje" żądanie.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.SuppressHostPrincipal();
// Other configuration code not shown...
}
}
Dodatkowe zasoby
filtry zabezpieczeń interfejsu API sieci Web ASP.NET (MAGAZYN MSDN)