W 1. części kursu – materiału na temat ucieczki od Razor’a, opowiem na temat porządków w projekcie, uporządkowaniu API oraz konfiguracji niezbędnych serwisów. Pacjent, na którym wykonujemy operacje, to moja strona – portfolio : LifeLike.pl
W chwili pisania artykułu, projekt leży na reactie, ale zanim przejdziemy do tej części, możliwe że zmieni się silnik i przeniosę się na np. Angular 5.
Podział projektu
Na #BoilingFrogs była ciekawa prelekcja na temat microserwisów oraz na temat Eustacha, który chciał zostać architektem -> rysować prostokąty i na tym zarabiać, ale zanim do tego dotrze, warto było najpierw podzielić nasz obecny monolit na moduły. Będąc 2 miesiące do przodu pomyślałem o tym, w moim projekcie, jednak zacząłem od dzielenia solucji na projekty.
Zacząłem od podstawowej rzeczy:
Solucja LifeLike, którą znajdziecie na githubie, podzieliłem na:
LifeLike.Data
Czyli, część która odpowiada za bazę danych, w moim przypadku był to Entity Framework 2.0 core oraz przypisanie do projektu na poziomie referencji. Czyli taka warstwa bazy danych. Klasy i referencje między nimi budowały bazę – czy to w MS SQL, które obecnie używam, czy na życzenie własne POSTGRES SQL lub MARIA DB. NoSQL się nie bawiłem. Jest to jednak bardzo wygodne rozwiązanie, bo mamy 1:1 baza danych = DTO. A wszelkie zmiany w klasach, wymagają wprowadzenia migracji danych w bazie danych. Wszystko jednak można osiągnąć z linii komend , nawet na linuxie i macu (na którym wciąż pracuję).
LifeLike.Repositories
Repozytoria danych, to tę warstwę będziemy “wstrzykiwać” potem do kontrolerów, jest ona odpowiedzialna za obróbkę danych pomiędzy UI lub serwisami a ORM bazy danych. Dodawanie, usuwanie, modyfikowanie, a nawet i szukanie, czego sobie Pan zapragnie 🙂
LifeLike.Services
Tutaj, znajdziem wszystkie ważne dla nas serwisy, którymi potem będziemy manipulowali. Tę warstwę, można też bardziej podzielić, a łączyć się z modułami, nie tylko za pomocą referencji, a także innymi sposobami jak servicebus, rest , czy nawet orientalnie po WCF. Podobnie, jak repozytoria, możemy wstrzykiwać tam istniejące serwisy lub repozytoria, a także, same serwisy możemy “wstrzykiwać” dalej. W projekcie czyjebnie, o którym pisałem wcześniej możecie więcej poczytać o samych serwisach.
LifeLike.Test
Ten projekt można podzielić na dodatkowe projekciki => czyli testy dla każdego modułu osobno, albo wszystko władować tutaj, jak kto woli 🙂 warstwa dla fetyszystów TDD oraz dla każdego, kto więcej chce przetestować swoje dzieła.
LifeLike.Web
Tutaj znajdujemy core aplikacji, oparty na net core 2.0. Tu wstrzykujemy dane, tworzymy api kontrolery, a także widok (choć , wg niektórych maniaków i to powinno się podzielić na FRONTEND (FRONTEND TO NIE PROGRAMOWANIE) oraz API.
Wszystko wg własnych preferencji oraz kosztów, bo jednak wypada “zainwestować” – mieć 2 osobne instancje.
WEB – konfiguracja
A skoro już przy Webie jesteśmy. W następnej części powiem więcej na temat konfiguracji frontendu, ale już teraz – PRZEDPREMIEROWO – dostaniecie mały gratis: konfigurację projektu pod projekty, typu SPA
Czystki
Skoro idziemy w stronę frameworków FRONTEND (FR.. a nie, powtarzam się), zacznijmy od wyrzucenia śmieci. W moim przypadku oberwie się Razor, czyli folder *View* ląduje w koszu. To samo, tyczy się kontrolerów, ale tu się wstrzymajcie od kasowania. Po prostu je przerobimy. W końcu w nowej stronie chcemy tą samą treść, więc po co robić 2. raz tę samą robotę! Po usunięciu kontrolerów, w większości edytorów kodu zauważycie teraz czerwień w kodzie. Świat zalany czerwienią … BŁĘDÓW. ERRORS EVERYWHERE!
Kontrolery
W pierwszej kolejności, znikają akcję zwane Index => Jak wiecie, do tej pory służyły wyłącznie do otwierania głównych stron razerowych. Jeśli macie, tam jakąś logikę, którą przekazywaliście do widoku, zamieńcie zwracany typ na:
ActionResult
a zwrócicie np. Json(dane do zwrotu – Zawołajcie mi tego Jasona!) lub Ok(dane) zamiast View(dane)
Przykład takiej metody u mnie to :
[HttpGet("Detail/{id}")] public async Task<IActionResult> Detail(long id) { try { var login= User.Identity.IsAuthenticated; var log = await _logger.Get(id); return Ok ( EventLogViewModel.Get(log)); } catch (Exception e) { await _logger.AddException(e); throw; } }
Atrybut HttpGet oznacza, że metodę wywołuje się za pomocą GET, parametr atrybutu, to ścieżka dostępu. W moim przypadku jest to np. localhost:5000/api/Log/Detail{id} , gdzie {id} to id logu, który chcemy pobrać.
Oczywiście, przekierowanie do kontrolera, też znajdziemy w atrybucie do całej klasy:
[Route("api/[controller]")] public class LogController : Controller
Startup – czyli władca wszystkiego
O samej konfiguracji wiele mówiłem, przy projekcie konfiguracji czyjebnie, o której wspominałem w tym poście.
W tamtym wpisie znajdziemy też co nieco na temat konfiguracji Swaggera, w którym zrobimy podgląd naszego API oraz przetestujemy metody. Aby zabezpieczyć API, dodałem autoryzację poprzez Json Web Token – JWT.
JWT – Czyli Tokenami człowiek żyje
W przypadku użycia Razora, wszelkie dane logowania oraz autoryzacji są po stronie ASP.NET MVC, jednak w przypadku, gdy komunikacja odbywa się na zewnątrz – w naszym przypadku Angular, React, a nawet aplikacja na telefon, warto pomyśleć o autoryzacji poprzez wysyłanie zaszyfrowanych tokenów, zamiast wrażliwych danych, typu User/Password. Tutaj z pomocą przychodzi nam JwtBearer, który w odpowiedzi do akcji rejestracji, zalogowania, wysyła do użytkownika Token, użytkownik używając zewnętrznej aplikacji, chcąc pytać API o wrażliwe dane, dodaje w headerze Token. Oczywiście, w chwili wycieku, haker dostaje tylko token, o określonej żywotności, a nie wspomniane wcześniej dane.
W Startup.cs i metodzie *ConfigureServices* dodajemy autentyfikację, za pomocą JwtBearer.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims services .AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(cfg => { cfg.RequireHttpsMetadata = false; cfg.SaveToken = true; cfg.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = Configuration["JwtIssuer"], ValidAudience = Configuration["JwtIssuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])), ClockSkew = TimeSpan.Zero // remove delay of token when expire }; });
Aby dostać token przy rejestracji, przenieśmy się do AccountController, gdzie w akcji login, po udanym logowaniu
zwracany jest token JWT.
if (result.Succeeded) { var user=_userManager.Users.SingleOrDefault(p=>p.UserName == model.Login); return Ok(GenerateJwtToken(model.Login, user)); } private string GenerateJwtToken(string login, IdentityUser user) { var claims = new List<Claim> { new Claim(JwtRegisteredClaimNames.Sub, login), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(ClaimTypes.NameIdentifier, user.Id), }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtKey"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var expires = DateTime.Now.AddDays(Convert.ToDouble(_configuration["JwtExpireDays"])); var token = new JwtSecurityToken( _configuration["JwtIssuer"], _configuration["JwtIssuer"], claims, expires: expires, signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); }
Sprawdzanie, czy Token należy do użytkownika itp., odbywa się już po stronie kontrolera oraz metod, które posiadają atrybuty.
[Authorize] //Sprawdza autoryzacje [AllowAnonymous] //Pozwala używać kontrolera, dla niezalogowanych - tzn nie posiadających tokena
Za pomocą User.Identity dowiemy się co nieco na temat osoby wywołującej akcję, możemy też ustalić, czy dostaje te same dane, co osoba zalogowana, czy może inną – ograniczoną treść. Pamiętajcie, że, metody i kontrolery z [Authize] i bez *AllowAnonymous* nie będą dostępne dla ludzi bez tokena */
var isLogin= User.Identity.IsAuthenticated;
.net Core daje dużo pozytywnych i niezbyt brzydkich featurów dla ludzi, zajmujących się backendem. Od tej chwili, nie musimy się bać, że każdy będzie mógł ukraść nam dane
Jeśli chcecie przetestować obecną wersję, użyjcie np. darmowego POSTMAN’a, gdzie po uzyskaniu tokenu za pomocą Login, możemy go użyć w zakładce authorization -> Bearer Token.
User-Secret – Czyli czas zataić informację
Moim największym błędem, które na szczęście nie miało konsekwencji, było trzymanie konfiguracji w App.Setting.json. Nie róbcie tego, zwłaszcza jak wrzucacie na publiczne repo. .Net Core niezależnie od platformy wprowadza tu ciekawe narzędzie: UserSecret, które dodajmy w pliku konfiguracyjnym projektu:
SecretManager przechowuje konfigurację w odpowiednim folderze, z kluczem , który dodajemy w Property Group projektu:
nr projektu
A sam plik json, znajdziemy w odpowiednim dla platformy folderze. Dostęp do wszystkiego jest z poiomu linii komend, a sam plik można edytować za pomocą dowolnego edytora.
Sama User Secret konfigurujemy w startup:
var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddEnvironmentVariables(); if (env.IsDevelopment()) builder.AddUserSecrets<Startup>(); Configuration = builder.Build();
Pliki z konfiguracją znajdziemy na:
Windows: %APPDATA%\microsoft\UserSecrets\\secrets.json Linux: ~/.microsoft/usersecrets//secrets.json macOS: ~/.microsoft/usersecrets//secrets.json
Szczegóły User Secrets znajdziecie na oficjalnej stronie Microsoft Docs:
(https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?tabs=visual-studio)
Tutaj polecam trzymać wszystkie kluczowe dane, jak dane połączeniowe, klucze itp. Potem, te dane można wrzucić np. do Azure, gdzie hostuję stronę albo ręcznie przerzucić do appsettings.json, jak używamy ftp 🙂
Goodbye Razor – Single Page Application
Ostatnim elementem mojej migracji jest ustawienie, skąd nasz strona ma brać widok.
W przeciwieństwie do wcześniej użytych rozwiązań, SPA wymaga małego rozszerzenia – dodatkowej paczki nuget
Microsoft.AspNetCore.SpaServices.Extensions
W *ConfigureServices* ustalamy, gdzie znajdziemy nasz “skompilowany” webowy frontend (tak wiem, skompilowany i frontend – CO ZA CZASY).
Mój frontendowy projekt trzymam w folderze ClientApp, gdzie znajduję package.json – listę paczek dostępnych w naszym projekcie (npm – taki nuget dla javascriptowcow)
services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; });
A w metodzie *Configure* dodatkowo ustalamy, by używał Statycznych plików SPA, oraz w przypadku fazy developmentu, jeśli chcemy zobaczyć wszelkie zmiany po każdej modyfikacji, bez restartu strony, robimy to za pomocą:
app.UseSpaStaticFiles(); app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } });
Podsumowanie
To tyle na temat migracji LifeLike z Razora na nowoczesny frontendowy framework, który nie wymaga przeładowania strony, a wymaga JavaScript – Sorry KrzaQ 🙂
P.S. Wiem, że MS wypuścił Blazor, dlatego mały żart z podtekstem erotycznym.
-Wiecie jak się nazywa grupa developerów używających Blazor?
-Blazzers !
Mam nadzieję, że tłumaczyć nie muszę, kim jest łysy z Brazzers.
Zapraszam też do subskrypcji kanału na YT oraz na blog kawowipodroznicy.pl