Dataniedziela, 9 sierpnia 2026 Czas06:29:08
← Aplikacje webowe

Lekcja 14. Logowanie i autoryzacja — ASP.NET Core Identity

Trudny / egzaminacyjny

Po 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

  1. Dodaj do paska nawigacji warunkowe wyświetlanie "Zaloguj się" (dla gości) albo "Witaj, [nazwa]! Wyloguj" (dla zalogowanych), korzystając z User.Identity.IsAuthenticated.

  2. Dodaj stronę/kontroler oznaczony [Authorize(Roles = "Admin")], dostępny WYŁĄCZNIE dla użytkowników z rolą "Admin" — wykorzystaj RoleManager<IdentityRole> do utworzenia roli i przypisania jej do wybranego konta testowego.

  3. 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.