To ósmy odcinek z dziesięciu w kursie podstaw ASP.NET Core MVC. Jeżeli prześledziłeś poprzednie lekcje, masz już działający magazyn produktów, bazę danych podpiętą przez EF Core i formularze CRUD-owe. Teraz pora na rzecz, bez której żadna serio aplikacja nie ruszy w świat — uwierzytelnianie użytkowników. Zamiast pisać to od zera (i robić po drodze pięć dziur bezpieczeństwa), użyjemy Identity Framework Core i w mniej niż dwadzieścia minut dorzucimy rejestrację, logowanie, wylogowanie oraz blokadę wybranych kontrolerów dla niezalogowanych.
Po co w ogóle Identity
Identity to gotowa warstwa uwierzytelniania od Microsoftu, która siedzi na EF Core i odwala brudną robotę: hashuje hasła, trzyma tabele AspNetUsers, AspNetRoles, AspNetUserRoles i parę innych, obsługuje role, claimy, potwierdzenia mailowe, blokady konta po N nieudanych próbach. Ty piszesz dwie metody w kontrolerze i formularz, reszta dzieje się pod maską.
Pisanie autentykacji ręcznie, bez solidnego powodu, to proszenie się o kłopoty.
Identitynie jest idealny, ale zamyka kilkanaście typowych ataków, o których większość juniorów nawet nie słyszała.
W kursowej aplikacji zakładamy prosty scenariusz — użytkownik rejestruje się przez login, e-mail i hasło, loguje się tym samym loginem, a nasz magazyn produktów ma być dostępny tylko dla zalogowanych. Tyle.
Instalacja paczek i pierwszy migration
Zaczynamy od NuGeta. W Package Manager dokładamy dwie paczki — Microsoft.AspNetCore.Identity.EntityFrameworkCore oraz Microsoft.AspNetCore.Identity.UI. Bardzo istotne: wersje muszą grać z wersją Twojej aplikacji. W kursie mamy .NET 5, więc paczki też idą w wersji 5.x. Jak pomieszasz wersje, wszystko wybuchnie dopiero na runtime, a komunikat błędu nie ułatwi Ci życia.
Dalej idziemy do Startup.cs, metody ConfigureServices, i konfigurujemy Identity. Trzeba tylko pamiętać, że kontekst bazy danych (AppDbContext) musi teraz dziedziczyć z IdentityDbContext, a nie ze zwykłego DbContext — inaczej EF nie wygeneruje tabel Identity.
// Startup.ConfigureServices
services.AddDefaultIdentity<IdentityUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<AppDbContext>();
W kursie odpuszczam restrykcyjne hasła (ustawiam RequireDigit = false, RequireUppercase = false itd.), bo chcę szybko przejść do rzeczy. Na produkcji zostaw domyślne albo dociśnij mocniej.
Po zmianie dziedziczenia AppDbContext robimy migrację:
Add-Migration AddIdentity
Update-Database
Efekt — w bazie pojawia się siedem nowych tabel (AspNetUsers, AspNetUserClaims, AspNetUserLogins, AspNetUserTokens, AspNetRoles, AspNetRoleClaims, AspNetUserRoles). Nas interesuje głównie pierwsza — tam wpadną konta po rejestracji.
Kontroler kont — Register, Login, Logout
Zamiast scaffoldować gotowe strony Identity (co też jest opcją), robimy własny AccountController z trzema akcjami: Register, Login, Logout. Każda ma wariant GET (pokaż formularz) i — oprócz Logout — wariant POST (odbierz dane).
Potrzebujemy dwóch ViewModeli w folderze Models:
public class RegisterViewModel
{
[Required]
public string Username { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
public class LoginViewModel
{
[Required]
public string Username { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
Widoki Register.cshtml i Login.cshtml to proste formularze silnie typowane przez @model, z Html.TextBoxFor dla loginu i maila oraz Html.PasswordFor dla hasła — dzięki temu pole hasła jest zamaskowane kropkami bez wymyślania koła na nowo.
W kontrolerze wstrzykujemy dwa serwisy przez konstruktor — UserManager<IdentityUser> do zakładania konta i SignInManager<IdentityUser> do logowania/wylogowania.
public class AccountController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
public AccountController(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (!ModelState.IsValid) return View(model);
var user = new IdentityUser
{
UserName = model.Username,
Email = model.Email
};
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
return RedirectToAction("Index", "Home");
return View(model);
}
}
Zwróć uwagę na dwie rzeczy. Po pierwsze — hasło przekazujemy osobnym parametrem do CreateAsync, żeby Identity mogło je zahashować, zanim cokolwiek wyląduje w bazie. Nigdy nie zapisuj hasła na IdentityUser wprost. Po drugie — metoda jest async i zwraca Task<IActionResult>. Jeżeli async/await brzmi Ci obco, potraktuj to na razie jak magię, którą rozbierzemy na części w osobnym odcinku.
Logowanie jest jeszcze prostsze:
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (!ModelState.IsValid) return View(model);
var result = await _signInManager.PasswordSignInAsync(
model.Username,
model.Password,
isPersistent: false,
lockoutOnFailure: false);
if (result.Succeeded)
return RedirectToAction("Index", "Home");
return View(model);
}
Parametr isPersistent decyduje, czy ciasteczko sesji ma przeżyć zamknięcie przeglądarki, a lockoutOnFailure — czy po paru nieudanych próbach Identity ma zablokować konto. Progi blokady konfiguruje się w Startup.
Wylogowanie to dosłownie jedna linijka — await _signInManager.SignOutAsync() i RedirectToAction("Index", "Home").
Middleware i ukrywanie przycisków w layoucie
W Startup.Configure, między UseRouting a UseAuthorization, musimy dodać app.UseAuthentication(). Bez tej linii całe logowanie przejdzie bez błędów, ale aplikacja nigdy nie pozna zalogowanego użytkownika. Kolejność jest kluczowa — najpierw UseAuthentication (kim jesteś?), potem UseAuthorization (czy wolno Ci tu wejść?).
W layoucie (_Layout.cshtml) podmieniamy nawigację, żeby w zależności od stanu użytkownika pokazywać różne linki:
@if (User.Identity.IsAuthenticated)
{
<a asp-controller="Account" asp-action="Logout">Wyloguj</a>
}
else
{
<a asp-controller="Account" asp-action="Login">Zaloguj</a>
<a asp-controller="Account" asp-action="Register">Rejestracja</a>
}
User.Identity.IsAuthenticated to gotowy property na ControllerBase i dostępny jest też w widokach — zero ceregieli.
Blokada kontrolera atrybutem [Authorize]
No i wisienka na torcie. Mamy już zalogowanego użytkownika, ale nasz WarehouseController nadal jest otwarty dla każdego, kto zna URL. Zabezpieczamy jedną linijką:
[Authorize]
public class AdminController : Controller { ... }
Atrybut [Authorize] możesz nałożyć na cały kontroler albo na wybraną akcję (np. tylko Delete i Edit, zostawiając Index publiczny). Gdy anonimowy użytkownik spróbuje wejść, Identity automatycznie przekieruje go na /Account/Login i wróci po udanym logowaniu.
Nie myl
[Authorize]z[Authorize(Roles = "Admin")]. Pierwszy wymaga tylko zalogowania, drugi — konkretnej roli. Role to temat na osobny odcinek.
Odpalamy aplikację, wylogowujemy się, klikamy „Dodaj produkt" — lądujemy na logowaniu. Logujemy się — wracamy na magazyn, możemy dodawać i usuwać. Wylogowanie znów zamyka drzwi. Dokładnie tego chcieliśmy.
Co dalej
W dziewiątym odcinku spróbujemy skonsumować jakieś zewnętrzne API — pokażę, jak wyciągnąć dane z publicznego serwisu i wyświetlić je w widoku. To samo w sobie jest krokiem w stronę realnych aplikacji, w których rzadko jesteśmy odcięci od zewnętrznego świata.
Jeżeli chcesz, żebym w osobnym materiale rozebrał na części async/await — daj znać w komentarzu pod odcinkiem. Wyszło tu kilka uproszczeń, które wyglądają magicznie, a pod maską są bardzo rozsądne.
Cały kod z tej lekcji znajdziesz na branchu Lekcja-8: github.com/jarmatys/asp-net-core-podstawy/tree/Lekcja-8. Pobierz, uruchom, porównaj ze swoją wersją — czasem najszybciej uczy się na diffie.



