Konfiguracja i opcje programu w .NET

Konfiguracja i opcje programu w .NET

Wstęp

Z tego artykułu dowiesz się na czym polega konfiguracja w .NET i jak odczytywać ustawienia na różne sposoby w swoich klasach (IOptions, IOptionsSnapshot, IOptionsMonitor), a także czym są opcje nazwane (named options).

Konfiguracja w .NET to nie tylko IConfigure, czy też IOptions. To naprawdę bardzo fajnie przemyślany mechanizm, który zdecydowanie warto poznać.

appsettings, czy nie appsettings… – czyli dostawcy konfiguracji

Być może nie wiesz, ale w .NET nie musisz trzymać konfiguracji w pliku appsettings. Co więcej, NIE jest to zalecane miejsce dla danych wrażliwych – tak jak skrupulatnie przekonują Cię o tym tutoriale na YouTube.

Jest wiele miejsc, w których możesz trzymać swoją konfigurację (zwłaszcza wrażliwe dane) i sam nawet możesz dopisać własne mechanizmy (np. odczyt konfiguracji z rejestru przy aplikacji desktopowej).

Taki mechanizm odczytywania danych nazywa się configuration provider – czyli dostawca konfiguracji. W .NET masz do dyspozycji kilku takich dostawców, którzy są w stanie pobrać Twoją konfigurację z miejsc takich jak:

  • plik appsettings.json
  • zmienne środowiskowe
  • Azure Key Vault (polecam do trzymania danych wrażliwych)
  • argumenty linii poleceń

Odczytywanie konfiguracji

Tworząc aplikację poprzez WebApplication.CreateBuilder lub Host.CreateDefaultBuilder, dodajemy m.in. kilku domyślnych providerów, którzy odczytują konfigurację z różnych miejsc i wszystko umieszczają w jednym obiekcie IConfiguration (a konkretniej, to providerzy ze swoimi danymi siedzą w IConfiguration). Konfiguracja jest dostarczana w dwóch etapach (w kolejności):

  1. Konfiguracja hosta, w której są odczytywane:
    • zmienne środowiskowe z prefixem DOTNET_
    • zmienne środowiskowe z prefixem DOTNET_ z pliku launchSettings.json
    • zmienne środowiskowe z prefixem ASPNETCORE_
    • zmienne środowiskowe z prefixem ASPNETCORE_ z pliku launchSettings.json (przy czym specjalna zmienna: ASPNETCORE_ENVIRONMENT wskazuje na aktualne środowisko (produkcja, development, staging -> to jest ładowane do HostingEnvironment. Jeśli tej zmiennej nie ma, to .NET traktuje to jako środowisko produkcyjne)
    • parametry z linii poleceń
  2. Konfiguracja aplikacji – w tym momencie znamy już HostingEnvironment (czyli wiadomo, czy to produkcja, develop, staging…)
    • konfiguracja z pliku appsettings.json
    • konfiguracja z pliku appsettings.environment.json – gdzie „environment” to określenie aktualnego środowiska („Production”, „Development”, „Staging”…)
    • konfiguracja z secrets.json
    • wszystkie zmienne środowiskowe

Pamiętaj, żeby nigdy nie odczytywać aktualnego środowiska z konfiguracji: Configuration["ASPNETCORE_ENVIRONMENT"], bo może to być błędne. Środowisko jest trzymane w IHostingEnvironment i to tego powinieneś używać do odczytu.

Dlaczego możesz się na tym przejechać? Załóżmy, że ktoś z jakiegoś powodu wpisze ustawienie ASPNETCORE_ENVIRONMENT do pliku appsettings.json. I już będzie klops. Bo owszem, ustawienie w obiekcie IConfiguration zostanie „nadpisane”, jednak IHostingEnvironment będzie trzymał zupełnie inne dane.

Co z tymi zmiennymi środowiskowymi i co to launchSettings.json?

Dlaczego .NET nie odczytuje zmiennych środowiskowych?

Czasami możesz odnieść takie wrażenie, że to po prostu nie działa. Też tak miałem, dopóki nie zdałem sobie sprawy z tego, jak naprawdę działają zmienne środowiskowe.

Program odczytuje te zmienne w momencie swojego uruchamiania. I to jest najważniejsze zdanie w tym akapicie. Zmienne środowiskowe nie są „aktualizowane” w aplikacji. Jeśli uruchomisz swoją aplikację z wiersza poleceń (dotnet run), to Twój program otrzyma takie zmienne jakie otrzymał wiersz poleceń podczas swojego uruchamiania.

Jeśli uruchamiasz program z VisualStudio, to Twój program otrzyma takie zmienne, jakie dostał VisualStudio podczas swojego uruchamiania.

Dlatego, jeśli zmieniasz wartości zmiennych środowiskowych, pamiętaj żeby zrestartować wiersz poleceń / Visual Studio. Wtedy Twoja aplikacja dostanie aktualne zmienne.

Jeśli zmieniasz zmienne na poziomie IIS, zrestartuj IIS.

Jest to pewna upierdliwość. Dlatego mamy plik launchSettings.json, w którym możesz sobie poustawiać różne zmienne środowiskowe. Te zmienne będą odczytywane podczas każdego uruchamiania Twojego programu – nie musisz niczego restartować.

Oczywiście pamiętaj, że plik launchSettings.json służy tylko do developmentu. Więc jeśli poustawiasz tam jakieś zmienne, których używasz, pamiętaj żeby ustawić je też na środowisku produkcyjnym.

Nie zdradzaj tajemnicy, czyli secrets.json

Domyślne pliki z ustawieniami – appsettings.json i appsettings.Development.json są przesyłane do repozytorium kodu. Jeśli pracujesz w zamkniętym zespole, to nie ma to większego znaczenia – dopóki w programie nie używasz jakiś swoich prywatnych subskrypcji.

Jeśli w plikach appsettings trzymasz dane wrażliwe (connection stringi, hasła, klucze), to miej świadomość, że one będą widoczne w repozytorium kodu i KAŻDY z dostępem będzie mógł z nich skorzystać (w szczególności GitHub).

Dlatego też powstał plik secrets.json. Aby go utworzyć/otworzyć, kliknij w Visual Studio prawym klawiszem myszy na swój projekt i z menu wybierz Manage User Secrets:

Możesz też użyć .NetCli i wykonać polecenie dotnet user-secrets

Wywołanie w VisualStudio otworzy Ci edytor tekstu taki sam jak dla pliku appsettings. Zresztą secrets.json ma dokładnie taką samą budowę.

Różnica między secrets.json a appsettings.json jest taka, że secrets.json nie znajduje się ani w katalogu z kodem (leży gdzieś tam w AppData), ani w repozytorium. Więc możesz sobie w nim bezkarnie umieszczać wszystkie klucze, hasła itd, których używasz w programie.

Oczywiście możesz mieć różne pliki sekretów w różnych projektach.

Gdzie dokładnie leży plik secrets.json?

W takiej lokalizacji: AppData\Roaming\Microsoft\UserSecrets\{Id sekretów}\secrets.json

Id sekretów to GUID, który jest przechowywany w pliku (csproj) konkretnego projektu.

Kolejność konfiguracji

Jak już zapewne wiesz – .NET odczytuje konfigurację w konkretnej kolejności – opisanej wyżej. A co jeśli w różnych miejscach (np. appsettings.json i secrets.json) będą ustawienia, które tak samo się nazywają? Nico. Ustawienia, które odczytują się później będą tymi aktualnymi. Czyli jeśli w pliku appsetting.json umieścisz:

"tajne-haslo" : ""

I to samo umieścisz w pliku secrets.json, który jest odczytywany później:

"tajne-haslo" : "admin123"

To z konfiguracji odczytasz „admin123”.

Dla wścibskich

Tak naprawdę te wartości nie są nadpisywane i przy odrobinie kombinowania możesz odczytać konkretne wartości z konkretnych miejsc (jako że IConfiguration nie trzyma bezpośrednio tych wartości, tylko ma listę ConfigurationProviderów). Domyślnie .NET szuka klucza „od tyłu” – w odwrotnej kolejności niż były dodawane do IConfiguration, ale moim zdaniem może to być szczegół implementacyjny, który w przyszłości może ulec zmianie. Jednak nie czytałem dokumentacji projektowej.

Pobieranie danych z konfiguracji

Prawdopodobnie to wiesz. Do klasy Startup wstrzykiwany jest obiekt implementujący IConfiguration i wtedy z niego możemy pobrać sobie dane, które nas interesują:

public IConfiguration Configuration { get; }

public Startup(IConfiguration configuration)
{
	Configuration = configuration;
}

//..
void Foo()
{
  string password = Configuration["tajne-haslo"];
}

Jeśli w konfiguracji masz bardziej zagnieżdżone dane, np:

{
  "EmailSettings": {
    "ServiceMailing": {
      "SmtpAddress": "https://smtp.example.com",
      "From": "Admin",
      "FromEmail": "admin@example.com"
    }
  }
}

posługujesz się dwukropkiem, żeby oddzielić kolejne poziomy, np:

string smtp = Configuration["EmailSettings:ServiceMailing:SmtpAddress"];

Jeśli chcesz tworzyć wielopoziomowe obiekty za pomocą zmiennych środowiskowych, to każdy poziom oddzielasz dwoma podkreślnikami: „__”, np:

setx EmailSettings__ServiceMailing__SmtpAddress "https://smtp.example.com" /M

To oczywiście podstawowe pobieranie danych z konfiguracji, przejdźmy teraz do fajniejszych rzeczy.

Tworzenie opcji dla programu

Dużo lepszym i fajniejszym rozwiązaniem jest tworzenie opcji dla komponentów Twojego programu. Załóżmy, że masz serwis do wysyłania e-maili. On może wyglądać tak:

public class EmailService
{
    const string OPTION_SMTP_ADDRESS = "https://smtp.example.com";
    const string OPTION_FROM = "Admin";
    const string OPTION_FROM_EMAIL = "admin@example.com";

    public void SendMail(string msg, string subject)
    {

    }
}

Albo jeszcze gorzej – tak:

public class EmailService
{
#if DEBUG
    const string OPTION_SMTP_ADDRESS = "https://smtp.local.example.com";
#else
    const string OPTION_SMTP_ADDRESS = "https://smtp.example.com";
#endif

    const string OPTION_FROM = "Admin";
    const string OPTION_FROM_EMAIL = "admin@example.com";

    public void SendMail(string msg, string subject)
    {

    }
}

I wtedy metoda SendMail będzie posługiwała się tymi nieszczęsnymi stałymi. Dużo lepszym rozwiązaniem byłoby trzymanie opcji w zupełnie innej klasie:

public class EmailOptions
{
    public string SmtpAddress { get; set; }
    public string From { get; set; }
    public string FromEmail { get; set; }
}

i wstrzyknięcie w jakiś sposób tych opcji do obiektu EmailService. Jest to możliwe. I zaraz pokażę Ci jak.

Konfiguracja opcji

Załóżmy, że Twój plik appsettings.json zawiera taki fragment:

"EmailSettings": {
  "SmtpAddress": "https://smtp.example.com",
  "From": "Admin",
  "FromEmail": "admin@example.com"
}

Tutaj najważniejsze jest to, jak masz nazwane poszczególne właściwości. Muszą być tak samo nazwane jak właściwości w Twojej klasie EmailOptions.

A w klasie EmailOptions to MUSZĄ być właściwości do publicznego odczytu i zapisu (nie mogą to być pola).

Jeśli już masz skonstruowaną klasę opcji (EmailOptions) i fragment konfiguracji (np. ten powyżej), możesz podczas konfiguracji serwisów dodatkowo skonfigurować te opcje:

services.Configure<EmailOptions>(Configuration.GetSection("EmailSettings"));

Czyli mówisz: „Klasa EmailOptions ma trzymać dane odczytane z sekcji w konfiguracji o nazwie „EmailSettings”.

Od teraz możesz klasę EmailOptions z wypełnionymi wartościami wstrzykiwać do swoich obiektów na trzy sposoby… Każdy z nich ma swoje wady i zalety.

Interfejs IOptions<T>

To pierwszy sposób pobrania opcji i chyba najprostszy. Wystarczy, że wstrzykniesz IOptions<T> do obiektu, w którym chcesz mieć swoją konfigurację:

public class EmailService
{
    EmailOptions options;

    public EmailService(IOptions<EmailOptions> options)
    {
        this.options = options.Value;  //pamiętaj, że opcje będziesz miał we właściwości Value
    }

    public void SendMail(string msg, string subject)
    {

    }
}

Zobacz jak sprytnie pozbyliśmy się tych brzydkich stałych z kodu na rzecz opcji trzymanych w odpowiednim obiekcie.

Plusy:

  • IOptions jest zarejestrowane jako singleton
  • Może być wstrzyknięte do każdego obiektu niezależnie od jego cyklu życia (Scoped, Singleton, czy Transient)

Minusy:

  • Odczytuje konfigurację TYLKO podczas uruchamiania systemu – to moim zdaniem jest najważniejsza kwestia. Przy niektórych opcjach to będzie wystarczające, przy innych nie.
  • Nie pozwala na „named options” (o tym za chwilę)

Interfejs IOptionsSnapshot<T>

Przykład wstrzyknięcia:

public EmailService(IOptionsSnapshot<EmailOptions> options)
{
    this.options = options.Value;
}

Czyli dokładnie tak samo. Różnice natomiast są trzy.

Plusy:

  • daje Ci aktualne opcje – nawet jeśli zmienią się w pliku – bez konieczności restartu aplikacji
  • obsługuje „named options”, o czym później

Minusy:

  • zarejestrowane jako scoped – odczytuje opcje z każdym requestem, jednak nie wstrzykniesz tego do serwisów rejestrowanych jako singleton.

Interfejs IOptionsMonitor<T>

To wygląda trochę jak hybryda dwóch poprzednich interfejsów.

  • jest rejestrowany jako singleton, więc może być wstrzyknięty do serwisu niezależnie od jego cyklu życia
  • potrafi zaktualizować opcje, gdy się zmienią – bez restartu aplikacji
  • obsługuje „named options”

Użycie tego jest nieco bardziej skomplikowane. Oto przykład:

IOptionsMonitor<EmailOptions> optionsMonitor;

public EmailService(IOptionsMonitor<EmailOptions> optionsMonitor)
{
    this.optionsMonitor = optionsMonitor;
}

public void SendMail(string msg, string subject)
{
    EmailOptions options = optionsMonitor.CurrentValue;
}

Pierwsza różnica jest taka, że nie trzymasz w swoim serwisie obiektu klasy EmailOptions tak jak to było do tej pory. Zamiast tego trzymasz cały monitor. A gdy potrzebujesz odczytać AKTUALNE opcje, posługujesz się właściwością CurrentValue tego monitora.

Teraz jeśli opcje fizycznie zostaną zmienione (np. w pliku appsettings.json), tutaj będziesz miał aktualne wartości – bez potrzeby restartowania aplikacji.

UWAGA! Zmiany zmiennych środowiskowych nie będą uwzględnione.

Czym są NamedOptions?

Spójrz na taki plik appsettings.json:

{
  "EmailSettings": {
    "ServiceMailing": {
      "SmtpAddress": "https://smtp.example.com",
      "From": "Admin",
      "FromEmail": "admin@example.com"
    },
    "NewsletterMailing": {
      "SmtpAddress": "https://smtp.news.example.com",
      "From": "John Rambo",
      "FromEmail": "john@news.example.com"
    }
  }
}

Masz tutaj różne ustawienia dla maili serwisowych i newslettera. Nie musisz tworzyć całej takiej struktury klas. Zwróć uwagę na to, że zarówno ServiceMailing jak i NewsletterMailing mają dokładnie takie same pola. Dokładnie też takie, jak klasa EmailOptions.

Możesz się tutaj posłużyć IOptionsSnapshot lub IOptionsMonitor, żeby wydobyć konkretne ustawienia (przypominam – IOptions nie obsługuje named options).

Najpierw trzeba jednak skonfigurować opcje, przekazując ich nazwy:

services.Configure<EmailOptions>("ServiceMailing", Configuration.GetSection("EmailSettings:ServiceMailing"));
services.Configure<EmailOptions>("NewsletterMailing", Configuration.GetSection("EmailSettings:NewsletterMailing"));

Zwróć uwagę tutaj na dwie rzeczy:

  • w pierwszym parametrze podajesz „nazwę zestawu opcji” – po tej nazwie będziesz później pobierał opcje do obiektu
  • w drugim pobierasz konkretną sekcję, w której są umieszczone te dane (tak jak do tej pory)

Teraz możesz odpowiednie opcje odczytać w taki sposób:

public class EmailService
{
    EmailOptions serviceMailOptions;
    EmailOptions newsletterMailOptions;

    public EmailService(IOptionsSnapshot<EmailOptions> options)
    {
        serviceMailOptions = options.Get("ServiceMailing");
        newsletterMailOptions = options.Get("NewsletterMailing");
    }
}

Odłóż wczytywanie opcji na później

Odraczanie czytania opcji może być przydatne dla twórców bibliotek. Więc jeśli tego nie robisz, możesz śmiało opuścić ten akapit. Jeśli Cię to interesuje, to rozwiń go:

Dobre praktyki

Jest kilka dobrych praktyk, które powinieneś stosować przy opcjach i naprawdę warto je stosować. Zdecydowanie mogą ułatwić Ci życie.

Twórz różne środowiska

Przede wszystkim, twórz w swoim projekcie różne środowiska. Development i Production to obowiązkowe minimum. Po prostu upewnij się, że masz takie pliki:

  • appsettings.json – ustawienia dla wersji produkcyjnej
  • appsettings.Development.json – ustawienia dla wersji developerskiej.

Tych plików możesz tworzyć znacznie więcej, np:

  • appsettings.Staging.json – ustawienia dla wersji przedprodukcyjnej (ostateczne testy przed wydaniem)
  • appsettings.Testing.json – jakieś ustawienia np. dla testów integracyjnych
  • appsettings.Local.json – jakieś typowe ustawienia dla środowiska lokalnego – Twojego komputera, na którym piszesz kod.

Pamiętaj, że o środowisku świadczy zmienna środowiskowa ASPNETCORE_ENVIRONMENT. Ona musi przyjąć jedną z nazw Twoich środowisk (Development, Production, Staging…). Jeśli tej zmiennej nie ma w systemie – uznaje się, że jest to wersja produkcyjna.

Nie posługuj się w kodzie dyrektywami w stylu:

#if DEBUG
  connectionString = Configuration["ConnectionStrings:DevConnectionString"];
#else
  connectionString = Configuration["ConnectionStrings:MainConnectionString"];
#endif

Zamiast tego używaj różnych środowisk… SERIO.

Nie trzymaj danych wrażliwych w appsetings

Pamiętaj, że pliki appsettings*.json lądują w repozytorium kodu. Chyba, że zignorujesz je w swoim systemie kontroli wersji. Jeśli tworzysz plik appsettings.Local.json – powinieneś automatycznie wyrzucać go z kontroli wersji.

Do trzymania wrażliwych danych używaj pliku secrets.json lub (w przypadku produkcji) – Azure KeyVault – jak to zrobić opiszę w osobnym artykule (zapisz się na newsletter lub polub stronę na fejsie, żeby go nie przegapić :)).

Nie używaj stringów (jako identyfikatorów) bezpośrednio

To chyba dotyczy wszystkiego – nie tylko opcji. Posługuj się w tym celu stałymi lub operatorem nameof. Np. zamiast wklepywać:

services.Configure<EmailOptions>(Configuration.GetSection("EmailSettings")); //nazwa sekcji na sztywno

wykorzystaj stałe:

public class EmailOptions
{
    public const string EmailOptionsSectionName = "EmailSettings";
    public string SmtpAddress { get; set; }
    public string From { get; set; }
    public string FromEmail { get; set; }
}

//
services.Configure<EmailOptions>(Configuration.GetSection(EmailOptions.EmailOptionsSectionName));

Sprawdzaj poprawność swoich opcji

Swoje opcje możesz walidować przez DataAnnotation (standard) lub FluentValidation (osobna biblioteka) i faktycznie powinieneś to robić, jeśli opcje mają jakieś ograniczenia lub z jakiegoś powodu mogą być niepoprawne.

To jednak temat na osobny artykuł, który możesz przeczytać tutaj.


To tyle, jeśli chodzi o zarządzanie opcjami w .NET. Jak pisałem wyżej – są jeszcze dwa aspekty, które na pewno będę chciał poruszyć w osobnych artykułach – walidajca opcji i odczytywanie opcji z Azure KeyVault. Być może napiszę też artykuł o tworzeniu własnego ConfigurationProvidera.

Jeśli znalazłeś w artykule jakiś błąd lub czegoś nie rozumiesz, koniecznie daj znać w komentarzu 🙂

Obrazek wyróżniający: Tło plik wektorowy utworzone przez mamewmy – pl.freepik.com

Podziel się artykułem na:
Ausweiss Kontrolle, czyli co to ten ClaimsPrincipal

Ausweiss Kontrolle, czyli co to ten ClaimsPrincipal

Wstęp

Gdy zaczynasz przygodę z mechanizmem Identity albo uwierzytelnianiem w .NET, możesz mieć problem ze zrozumieniem czym jest ClaimsPrincipal, claimsy i wszystko co z tym związane. W tym artykule rozwiewam wszelkie wątpliwości. Temat jest dość prosty, a więc artykuł będzie dość krótki.

Dokumenty do kontroli

Krótko mówiąc, ClaimsPrincipal to zbiór danych, który przechowuje informacje na temat zalogowanego użytkownika. Pewnie chcesz zadać pytanie – czy to nie może być moja super klasa User? Może, zwłaszcza jeśli chcesz walczyć z materią zamiast programować 🙂 ClaimsPrincipal to pewien standardowy sposób przechowywania i przesyłania danych. Poza tym w pewnych sytuacjach naprawdę jest dużo wygodniejszy. Chociaż będziesz tworzył swoją super klasę User na podstawie ClaimsPrincipal, to jednak to właśnie jest podstawowy sposób trzymania danych o zalogowanym użytkowniku.

Scenariusz

Wyobraź sobie, że Twoja firma wysyła Cię do innej dużej firmy w ramach jakiejś współpracy. Nikt Cię tam nie zna, a musisz mieć pewne uprawnienia (np. możliwość wejścia do sali konferencyjnej). Podchodzisz do strażnika i pokazujesz mu swój dowód osobisty – logujesz się swoimi poświadczeniami (w tym przypadku dowód osobisty to Twój login i hasło)

Strażnik sprawdza dane i widzi, że faktycznie miałeś przyjść i masz w tej firmie jakąś rolę. Wydaje Ci więc coś w rodzaju dowodu tożsamości – to może być identyfikator, karta wstępu, cokolwiek. Załóżmy, że to będzie tymczasowa karta wstępu z paskiem magnetycznym.

Ta karta wstępu to ClaimsIdentity (tożsamość). Dane zawarte na tej karcie (imię, nazwisko, rola) – to są Claimsy. A Ty jako posiadacz takiego dokumentu jesteś ClaimsPrincipal.

Gdybyś chodził po firmie tylko z dowodem osobistym, musiałbyś pokazywać go na każdym kroku i miałbyś utrudnione poruszanie się po budynku. Natomiast taka karta wstępu uwierzytelnia Cię i automatycznie daje Ci dostęp do pewnych pomieszczeń (np. sali konferencyjnej).

Podsumowując:

  • ClaimsPrincipal – osoba (użytkownik, system), posiadająca przynajmniej jeden dowód tożsamości
  • ClaimsIdentity – dowód tożsamości tej osoby, może ich być kilka (tak jak w życiu możesz posiadać dowód osobisty, prawo jazdy, paszport…)
  • Claims – dane z tego dowodu

Tworzenie ClaimsPrincipal

W mechanizmie uwierzytelniania, który opisałem tutaj, ręcznie musisz zalogować użytkownika, tworząc ClaimsPrincipal. Microsoft Identity robi to automatycznie (Ty tylko musisz ewentualnie odczytać pewne dane).

Aby utworzyć ClaimsPrincipal, musisz sobie najpierw odpowiedzieć na pytanie – jakie dane chcesz mieć w nim dostępne. To pytanie właściwie odnosi się do rodzaju dokumentu tożsamości, jaki będzie dodany do ClaimsPrincipal.

Tworzenie Claim

Na początku powinieneś utworzyć listę Claimów. Claim w mechanizmie uwierzytelniania pełni kluczową rolę.

Każdy Claim ma swój typ i wartość. Oczywiście jest kilka konstruktorów i dodatkowych właściwości Claima, ale skupimy się na tych podstawowych – typ i wartość. Reszta, jak np. wydawca sama się opisuje. Jeśli chcesz się wczytać bardziej technicznie w Claim, zobacz ten artykuł w Microsoft.

Typ claima to string określający co jest jego zawartością (zazwyczaj w formie URI). I w zasadzie możesz sobie podać tam co Ci się żywnie podoba, np:

Claim shoeSizeClaim = new Claim("rozmiar-buta", "46");

JEDNAK są pewne zdefiniowane typy, którymi powinieneś się posługiwać. Są dostępne z poziomu klasy ClaimTypes. Poniżej prezentuję te, które uważam za najważniejsze. Jeśli sądzisz, że ta lista powinna być rozszerzona – koniecznie daj znać w komentarzu:

ZnaczeniePole w ClaimTypesUwagi
Adres e-mailClaimTypes.Email
Nazwa użytkownikaClaimTypes.UserName.NET Identity używa tego jako nazwy użytkownika. Ale inni wystawcy mogą trzymać tu imię i nazwisko albo jakiś własny ciąg dla customowych danych
Id użytkownikaClaimTypes.NameIdentifierPamiętaj, że to jest id użytkownika w postaci stringu. To może być zarówno int jak i GUID
ImięClaimTypes.GivenName
NazwiskoClaimTypes.Surname
Wspólna nazwa użytkownikaClaimTypes.CommonNameTo jest nazwa użytkownika, która powinna być taka sama we wszystkich systemach. Załóżmy, że użytkownik ma login na Facebooku „PanWłodek”, natomiast na google „PaniWiesia”. Wspólna nazwa powinna określać ten sam nick na Facebooku i Google, np: „Janek123”. Oczywiście jeśli inne systemy pozwalają na taką dodatkową daną. To coś jak numer SKU w systemach magazynowych.
Rola użytkownikaClaimTypes.RoleRola użytkownika w systemie (np. admin, edytor itd). Oczywiście może być kilka claimów tego typu, ponieważ użytkownik może mieć wiele ról.
Unikalny identyfikator sesjiClaimTypes.SidTo jest unikalny identyfikator sesji dla użytkownika na danym urządzeniu.

Utwórzmy teraz przykładową listę claimów:

List<Claim> claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
    new Claim(ClaimTypes.Email, user.Email),
    new Claim(ClaimTypes.Role, "Writer"),
    new Claim(ClaimTypes.Role, "Moderator"),
    new Claim("numer-buta", user.ShoeNo)
};

Załóżmy, że user to jakiś użytkownik, którego próbujemy zalogować. Role Writer i Moderator to jakieś role w Twoim systemie.

Mając listę claim’ów, możemy teraz utworzyć dowód tożsamości – ClaimsIdentity:

ClaimsIdentity identity = new ClaimsIdentity(claims);

Mając ClaimsIdentity, możesz utworzyć ClaimsPrincipal:

ClaimsPrincipal principal = new ClaimsPrincipal(identity);

Jak tego używać?

Przede wszystkim, jeśli używasz mechanizmu Identity, to nie musisz tworzyć ClaimsPrincipal. To robi mechanizm. Jeśli używasz czystego uwierzytelniania, tak jak opisałem tutaj, podczas logowania musisz utworzyć ten obiekt i przekazać go dalej (np. do ciastka logowania).

Natomiast ważna rzecz – mechanizm uwierzytelniania (z którego korzysta też Identity) podczas ładowania strony (czy też endpointa w WebApi) automatycznie tworzy ten obiekt na podstawie danych, które otrzyma (z ciasteczka, tokenu, czy innego schematu). To być może brzmi niezbyt jasno. Lepiej to opisałem w artykule o uwierzytelnianiu.

W każdym razie pamiętaj, że w HttpContext.User masz w pełni gotowy obiekt ClaimsPrincipal, który możesz wykorzystywać.

Sprawdzenie, czy użytkownik posiada Claim

Sprawdźmy, czy użytkownik ma zapisany numer buta. Można to zrobić na dwa sposoby. Albo użyjesz LINQ i zrobisz to wygodniej, albo ręcznie sprawdzisz wszystkie Claimy 🙂

if(principal.HasClaim(c => c.Type == "numer-buta"))
  //posiada
else
  //nie posiada

Ta instrukcja pod spodem sprawdzi WSZYSTKIE ClaimsIdentity (tożsamości), które posiada użytkownik. Jeśli któryś z nich ma taką daną jak „numer-buta”, HasClaim zwróci true.

Pobranie wartości konkretnego Claim

Spróbujmy teraz pobrać Id użytkownika. Jak pisałem wyżej – powinno to być zapisane jako NameIdentifier:

Claim idClaim = principal.FindFirst(ClaimTypes.NameIdentifier);

Pamiętaj tylko, że jeśli użytkownik nie ma Claima tego typu, FindFirst zwróci null. Dlatego też powinieneś się zabezpieczyć przed takim scenariuszem. Oczywiście NameIdentifier powinien zawsze być obecny, jeśli użytkownik jest zalogowany.

Zawsze możesz też stworzyć rozszerzenie (extension class), które pomoże Ci pobierać odpowiednie wartości, np:

public static class ClaimsPrincipalExtensions
{
    public static Guid GetUserId(this ClaimsPrincipal cp)
    {
        Claim idClaim = cp.FindFirst(ClaimTypes.NameIdentifier);
        return idClaim == null ? Guid.Empty : Guid.Parse(idClaim.Value);
    }

    public static int GetShoeSize(this ClaimsPrincipal cp)
    {
        Claim claim = cp.FindFirst("rozmiar-buta");
        if (claim == null)
            return 0;

        int result = 0;
        if (!int.TryParse(claim.Value, out result))
            return 0;
        else
            return result;
    }
}

Utworzenie takiej klasy to dobra praktyka, jeśli używasz Claimów trochę bardziej niż w najprostszej aplikacji.

Czy użytkownik jest zalogowany?

Czasem zachodzi potrzeba sprawdzenia, czy użytkownik jest zalogowany – np. z poziomu RazorPage. Chociaż częściej będziesz się posługiwał atrybutem Authorize, to jednak możesz sprawdzić to w kodzie.

Obiekt User w HttpContext będzie obecny zawsze. Zatem sprawdzenie, czy jest nullem nie ma żadnego sensu, bo taki warunek nigdy nie będzie spełniony. Natomiast możesz na kilka sposobów sprawdzić, czy użytkownik jest zalogowany (poniżej pokazuję Ci przykład kodu, który możesz umieścić w swoim extension class):

public static bool IsLoggedIn(this ClaimsPrincipal cp)
{
    if (cp.Identity == null)
        return false;
    return cp.Identity.IsAuthenticated;
}

Jako że ClaimsPrincipal może mieć kilka ClaimsIdentity, właściwość Identity zwraca Ci pierwsze ClaimsIdentity z listy. Oczywiście wcale nie musi być żadnej tożsamości.

Każde ClaimsIdentity posiada właściwość IsAuthenticated, która jedyne co robi, to sprawdza, czy właściwość AuthenticationType ma jakąś wartość.

AuthenticationType to nazwa schematu, którym dana tożsamość (ClaimsIdentity) została uwierzytelniona. Więcej o tym w artykule o uwierzytelnianiu. To może być np. „cookie”, „bearer” itd.

Oczywiście możesz sprawdzić konkretne ClaimsIdentity, np:

public static bool IsLoggedIn(this ClaimsPrincipal cp)
{
    ClaimsIdentity? ci = cp.Identities.FirstOrDefault(id => id.AuthenticationType == "facebook");
    return ci != null;            
}

Tutaj sprawdzam, czy istnieje ClaimsIdentity o odpowiedniej wartości AuthenticationType. Nie sprawdzam już, co zwraca właściwość IsAuthenticated, bo jak napisałem wyżej – ona sprawdza tylko czy wartość AuthenticationType nie jest pusta. Więc jeśli na liście tożsamości jest tożsamość o zadanym AuthenticationType, znaczy to że użytkownik jest zalogowany.

Czy użytkownik ma odpowiednią rolę

To też możesz sprawdzić, używając ClaimsPrinciple:

if(User.IsInRole("moderator"))
    //ma rolę
else
    //nie ma

Metoda IsInRole przeleci wszystkie Claimy o type Role.

Uwaga, tutaj standardową techniką jest też posłużenie się atrybutem Authorize z odpowiednią rolą, ale czasem chcesz sprawdzić to w kodzie.


To chyba wszystko, co Ci potrzebne, żeby zacząć świadomie działać z ClaimsPrincipal. Dziękuję Ci za przeczytanie tego artykułu. Jeśli masz jakieś pytania, czegoś nie rozumiesz lub znalazłeś błąd, koniecznie daj znać w komentarzu 🙂

Obraz wyróżniający: Makieta pliki psd utworzone przez Xvect intern – pl.freepik.com

Podziel się artykułem na:
Middleware pipeline, czyli rurociąg w .NET Core

Middleware pipeline, czyli rurociąg w .NET Core

Wstęp

Z tego tekstu dowiesz się czym jest middleware pipeline w .NET, jak go konfigurować i jak nim zarządzać. Większość blogów jakie widziałem, traktowały ten temat po macoszemu, ja postaram się go opisać dogłębnie. W końcu middleware pipeline to serce internetowych aplikacji w .NET.

Co to jest

Spójrzmy najpierw na middleware. Co to? To nic innego jak metoda (Action<HttpContext>), która w jakiś sposób przetwarza żądanie HTTP. Może je odczytywać, może zapisać coś w odpowiedzi na to żądanie, a także w jakiś sposób na nie zareagować. Więc – middleware to jest metoda, która przyjmuje w parametrze HttpContext (i dodatkowo kolejny middleware). Profesjonalnie nazywa się „oprogramowaniem pośredniczącym”, ale my będziemy mówić „komponent”. Bo w gruncie rzeczy tym właśnie jest.

To teraz czym jest pipeline? Po polsku nazywa się to „potokiem”… No i cześć… Można powiedzieć, że to taki „rurociąg” przez który przechodzi żądanie HTTP, a w rurociągu żyją sobie komponenty middleware.

Innymi słowy można powiedzieć, że to coś w rodzaju taśmy produkcyjnej.

Middleware pipeline jako taśma produkcyjna

Wyobraź sobie fabrykę, która produkuje różne surówki. W pewnym momencie dostaje żądanie wyprodukowania surówki z buraków.

Pierwsza osoba, która stoi przy taśmie produkcyjnej (komponent) przygotowuje buraki na podstawie tego żądania – obiera je i myje. Gdy wykona swoją robotę, przekazuje żądanie dalej – do kolejnej osoby.