Lekcja 14. Logowanie i autoryzacja — ASP.NET Core Identity
Trudny / egzaminacyjnyPo co się tego uczymy?
Prawie każda poważna aplikacja webowa rozróżnia użytkowników — gość, zalogowany użytkownik, administrator. Pisanie własnego systemu logowania od zera (hashowanie haseł, blokowanie po kilku nieudanych próbach, resetowanie hasła) jest trudne i pełne pułapek bezpieczeństwa. ASP.NET Core Identity to gotowy, sprawdzony w boju system, który rozwiązuje ten problem — ta lekcja pokazuje jego podstawy: rejestrację, logowanie i ograniczanie dostępu do wybranych stron.
Teoria
ASP.NET Core Identity to gotowy framework do zarządzania kontami użytkowników, wbudowany w ASP.NET Core — dostarcza tabele bazy danych (użytkownicy, role, hasła w bezpiecznej, zahashowanej formie), gotowe strony logowania/rejestracji, oraz mechanizm ciasteczek uwierzytelniających (opartych o te same podstawy co sesje z poprzedniej lekcji). Dodaje się go do projektu przez builder.Services.AddDefaultIdentity<IdentityUser>().AddEntityFrameworkStores<NazwaDbContext>() — Identity WYMAGA bazy danych (Entity Framework Core, pełne omówienie w lekcji 15) do przechowywania kont.
Nigdy nie przechowuj haseł w czystym tekście. Kluczowa zasada bezpieczeństwa: hasła NIGDY nie są zapisywane w bazie danych "jak są" — Identity automatycznie je haszuje (nieodwracalne przekształcenie matematyczne) przed zapisem. Przy logowaniu, wpisane hasło jest haszowane TĄ SAMĄ funkcją i porównywane z zapisanym hashem — jeśli hashe się zgadzają, hasło było poprawne. Nikt (nawet administrator bazy danych) nie jest w stanie odczytać oryginalnego hasła z bazy, tylko sprawdzić, czy podane hasło do niego pasuje.
Kluczowe klasy i atrybuty:
| Element | Rola |
|---|---|
UserManager<IdentityUser> |
zarządzanie kontami: tworzenie, wyszukiwanie, zmiana hasła — wstrzykiwane przez Dependency Injection |
SignInManager<IdentityUser> |
obsługa procesu logowania/wylogowania (tworzenie i usuwanie ciasteczka uwierzytelniającego) |
[Authorize] |
atrybut na stronie/kontrolerze — BLOKUJE dostęp dla niezalogowanych użytkowników (przekierowanie na stronę logowania) |
[Authorize(Roles = "Admin")] |
wariant wymagający KONKRETNEJ roli, nie tylko samego zalogowania |
[AllowAnonymous] |
jawnie zezwala na dostęp BEZ logowania (przydatne np. gdy cały kontroler ma [Authorize], ale jedna akcja ma być publiczna) |
User.Identity.IsAuthenticated |
sprawdzenie w widoku/kodzie, czy bieżący użytkownik jest zalogowany |
Proces rejestracji. await _userManager.CreateAsync(nowyUzytkownik, haslo) tworzy konto — Identity samo sprawdza wbudowane reguły siły hasła (minimalna długość, wymagane cyfry/znaki specjalne — konfigurowalne) i zwraca IdentityResult z ewentualną listą błędów, jeśli hasło jest za słabe albo e-mail już istnieje.
Proces logowania. await _signInManager.PasswordSignInAsync(email, haslo, zapamietaj: true, lockoutOnFailure: true) sprawdza dane logowania i, jeśli poprawne, tworzy ciasteczko uwierzytelniające. lockoutOnFailure: true włącza AUTOMATYCZNE blokowanie konta po kilku nieudanych próbach logowania z rzędu — ważne zabezpieczenie przed atakiem brute-force (próbowaniem wielu haseł po kolei).
Schemat
Rejestracja:
Formularz (Email, Hasło) → POST
│
▼
_userManager.CreateAsync(nowyUzytkownik, haslo)
│
├── hasło zbyt słabe / e-mail zajęty → błędy walidacji
└── sukces → konto zapisane w bazie (hasło ZAHASZOWANE)
Logowanie:
Formularz (Email, Hasło) → POST
│
▼
_signInManager.PasswordSignInAsync(email, haslo, ...)
│
├── niepoprawne dane / konto zablokowane → komunikat błędu
└── sukces → ciasteczko uwierzytelniające ustawione
Kolejne żądania:
Strona z [Authorize]
│
niezalogowany ──► przekierowanie na /Identity/Account/Login
│
zalogowany ──► dostęp dozwolony, User.Identity.IsAuthenticated == true
Przykład z życia
Panel klienta w sklepie internetowym (historia zamówień, dane dostawy) jest dostępny WYŁĄCZNIE po zalogowaniu, a panel administracyjny zarządzania produktami wymaga dodatkowo roli "Admin" — dokładnie ten wzorzec (logowanie + role) realizuje ASP.NET Core Identity, zwalniając programistę z konieczności ręcznego pisania bezpiecznego systemu haseł od zera.
Pages/Konto/Logowanie.cshtml + fragment _Layout.cshtml
<!-- Pages/Konto/Logowanie.cshtml -->
@page
@model LogowanieModel
<h1>Logowanie</h1>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post">
<div>
<label asp-for="Email"></label>
<input asp-for="Email" />
</div>
<div>
<label asp-for="Haslo"></label>
<input asp-for="Haslo" type="password" />
</div>
<button type="submit">Zaloguj się</button>
</form>
<!-- Pages/Shared/_Layout.cshtml - fragment paska nawigacji -->
@if (User.Identity?.IsAuthenticated == true)
{
<span>Witaj, @User.Identity.Name!</span>
<form method="post" asp-page="/Konto/Wyloguj"><button type="submit">Wyloguj</button></form>
}
else
{
<a asp-page="/Konto/Logowanie">Zaloguj się</a>
}
Pages/Konto/Logowanie.cshtml.cs + przykład [Authorize]
// Pages/Konto/Logowanie.cshtml.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
namespace MojaStronaApp.Pages.Konto;
public class LogowanieModel : PageModel
{
private readonly SignInManager<IdentityUser> _signInManager;
public LogowanieModel(SignInManager<IdentityUser> signInManager)
{
_signInManager = signInManager;
}
[BindProperty]
[Required(ErrorMessage = "Podaj e-mail.")]
[EmailAddress]
public string Email { get; set; } = "";
[BindProperty]
[Required(ErrorMessage = "Podaj hasło.")]
public string Haslo { get; set; } = "";
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var wynik = await _signInManager.PasswordSignInAsync(
Email, Haslo, isPersistent: true, lockoutOnFailure: true);
if (!wynik.Succeeded)
{
ModelState.AddModelError(string.Empty, "Nieprawidłowy e-mail lub hasło.");
return Page();
}
return RedirectToPage("/Index");
}
}
// ---------------------------------------------------------------------
// Strona chroniona logowaniem
[Authorize]
public class PanelKlientaModel : PageModel
{
public void OnGet()
{
// User.Identity.Name dostępne tylko dla zalogowanych,
// niezalogowani zostaną automatycznie przekierowani na stronę logowania
}
}
Komentarz i wyjaśnienie kodu
SignInManager<IdentityUser> jest WSTRZYKIWANY przez konstruktor (Dependency Injection — ten sam mechanizm, który poznałeś przy powiadomieniach lokalnych w MAUI) — ASP.NET Core sam tworzy i dostarcza tę instancję, bez potrzeby ręcznego new SignInManager(...).
ModelState.AddModelError(string.Empty, "...") dodaje komunikat błędu NIE przypisany do KONKRETNEGO pola (pusty string jako klucz) — pojawi się w asp-validation-summary, ale nie przy żadnym pojedynczym polu, bo z założenia niepoprawne dane logowania nie wskazują, KTÓRE dokładnie pole (e-mail czy hasło) jest błędne — ze względów bezpieczeństwa NIE ujawniamy, czy to e-mail nie istnieje, czy hasło jest złe (utrudnia to atakującemu zgadywanie zarejestrowanych adresów e-mail).
[Authorize] nad klasą PanelKlientaModel wystarczy, żeby CAŁA strona wymagała zalogowania — próba wejścia bez zalogowania automatycznie przekierowuje na stronę logowania Identity, z parametrem ReturnUrl pozwalającym po udanym zalogowaniu wrócić DOKŁADNIE tam, gdzie użytkownik pierwotnie chciał trafić.
Ćwiczenie samodzielne
Utwórz nowy projekt ASP.NET Core z szablonem uwzględniającym Identity (opcja "Individual Accounts" przy tworzeniu projektu w Visual Studio) i przeanalizuj wygenerowane strony logowania/rejestracji w folderze Areas/Identity/Pages/Account/. Zarejestruj testowe konto i sprawdź w bazie danych (SQLite/SQL Server LocalDB, w zależności od konfiguracji), że hasło jest zapisane jako długi, nieczytelny hash, a nie zwykły tekst.
Zadania do pracy własnej
Dodaj do paska nawigacji warunkowe wyświetlanie "Zaloguj się" (dla gości) albo "Witaj, [nazwa]! Wyloguj" (dla zalogowanych), korzystając z
User.Identity.IsAuthenticated.Dodaj stronę/kontroler oznaczony
[Authorize(Roles = "Admin")], dostępny WYŁĄCZNIE dla użytkowników z rolą "Admin" — wykorzystajRoleManager<IdentityRole>do utworzenia roli i przypisania jej do wybranego konta testowego.Zbuduj kompletny mechanizm rejestracji + logowania + strefy chronionej ("Mój profil", dostępnej tylko po zalogowaniu, wyświetlającej adres e-mail zalogowanego użytkownika) w JEDNYM, spójnym projekcie — z pełną walidacją formularzy rejestracji (Data Annotations: e-mail poprawny, hasła się zgadzają) i poprawnym przekierowaniem po udanym logowaniu z powrotem na stronę, którą użytkownik pierwotnie próbował odwiedzić (parametr
ReturnUrl).
Typowe błędy
Próba samodzielnego "wynajdywania koła na nowo" — pisanie własnego systemu haseł (własne hashowanie, własne tabele użytkowników) zamiast użycia sprawdzonego, przetestowanego Identity — znacznie zwiększa ryzyko subtelnych błędów bezpieczeństwa.
Zapominanie o [Authorize] na stronach, które faktycznie powinny być chronione — bez tego atrybutu strona jest dostępna dla KAŻDEGO, niezależnie od stanu logowania, nawet jeśli wyświetla dane osobiste.
Ujawnianie w komunikacie błędu logowania, CZY to e-mail, czy hasło było niepoprawne — zamiast ogólnego "Nieprawidłowy e-mail lub hasło", osobne komunikaty ("Ten e-mail nie istnieje" / "Złe hasło") ułatwiają atakującemu sprawdzenie, które adresy e-mail są zarejestrowane w systemie.
Nawiązanie do egzaminu zawodowego
To bezpośrednia realizacja INF.04.7.3 "programowanie systemów logowania" i "obsługa różnych poziomów dostępu w witrynie" — jeden z najważniejszych, praktycznych elementów zaawansowanych aplikacji webowych. Kolejna lekcja przechodzi do Entity Framework Core — mechanizmu bazy danych, z którego Identity korzysta pod spodem do przechowywania kont użytkowników.