Aplikacja webowa - trzy warstwy zamiast instalacji

Aplikacja webowa działa na serwerze, a użytkownik widzi jej wynik w oknie przeglądarki. Kod interfejsu w HTML i JavaScript trafia do przeglądarki przy każdym wejściu na stronę, natomiast reguły biznesowe oraz dostęp do danych zostają po stronie serwera. Na stacji roboczej nie instaluje się nic poza przeglądarką, a jedna wersja programu obsługuje wszystkich użytkowników naraz.

Klasyczny podział obejmuje przeglądarkę oraz dwa serwery: aplikacyjny i bazodanowy. Warstwy mogą działać na jednej maszynie albo na kilku, co wpływa na wydajność i bezpieczeństwo, ale nie zmienia logiki podziału. Opis systemu reklamacji RMA system pokazuje ten układ na przykładzie aplikacji ASP.NET z bazą Microsoft SQL Server.

WarstwaZadaniePrzykładowa technologia
Przeglądarkawyświetla interfejs, wstępnie waliduje pola i wysyła żądaniaHTML, CSS, JavaScript
Serwer WWWkończy połączenie HTTPS i przekazuje żądanie do aplikacjiIIS
Serwer aplikacjiwykonuje reguły biznesowe i buduje odpowiedźASP.NET, C#
Baza danychprzechowuje dane i pilnuje spójności transakcjiSQL Server, T-SQL

Reguła: każdą walidację wykonaną w przeglądarce trzeba powtórzyć na serwerze, bo żądanie HTTP można wysłać z pominięciem interfejsu.

Granice między warstwami

Granicę między warstwami wyznacza sieć, a każde jej przekroczenie kosztuje czas. Ekran, który przy otwarciu wysyła kilkadziesiąt osobnych żądań, będzie wolniejszy od ekranu z jednym zapytaniem zwracającym gotowy zestaw danych. Dlatego projektanci grupują dane w odpowiedzi i przenoszą filtrowanie oraz sortowanie na serwer, zamiast pobierać cały zbiór do przeglądarki.

Podobnie wygląda podział odpowiedzialności za dane. Przeglądarka przechowuje tylko to, co jest potrzebne do wyświetlenia bieżącego widoku, a źródłem prawdy pozostaje baza. Gdy dwie osoby edytują ten sam rekord, rozstrzyga serwer, nie interfejs.

Ilustracja aplikacji internetowej otwartej na laptopie, tablecie i telefonie jednocześnie
Ten sam interfejs w przeglądarce na różnych urządzeniach - warstwa prezentacji działa u klienta, a dane i logika zostają na serwerze.

Cykl żądanie-odpowiedź w HTTP

Protokół HTTP jest bezstanowy: serwer nie pamięta poprzedniego żądania, dopóki aplikacja sama tego nie zadba. Każde kliknięcie przycisku, otwarcie listy albo zapis formularza jest osobną wymianą, w której przeglądarka wysyła metodę i adres razem z nagłówkami, a serwer odsyła kod statusu oraz treść. Specyfikację semantyki opisuje dokument RFC 9110, a praktyczne omówienie znajduje się w przeglądzie HTTP w dokumentacji MDN.

MetodaTypowe użycie w aplikacjiPowtórzenie żądania
GETpobranie listy lub szczegółów rekordubezpieczne, nie zmienia danych
POSTzapis nowego dokumentu, uruchomienie operacjimoże utworzyć duplikat
PUTzastąpienie rekordu całością nowych danychidempotentne
DELETEusunięcie lub dezaktywacja rekorduidempotentne

Asynchroniczne wywołania z przeglądarki

Formularz wysyłany tradycyjnie odświeża całą stronę. Aplikacje z rozbudowanym interfejsem używają wywołań asynchronicznych (AJAX), w których skrypt wysyła żądanie w tle i podmienia tylko fragment widoku. Poniższy przykład jest poglądowy: wysyła zapis dokumentu i obsługuje odpowiedź z błędem walidacji.

async function zapiszDokument(dokument) {
  const odp = await fetch('/api/dokumenty', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(dokument)
  });
  if (odp.status === 400) {
    const bledy = await odp.json();   // lista pól z komunikatami
    pokazBledy(bledy);
    return;
  }
  if (!odp.ok) throw new Error('Błąd serwera: ' + odp.status);
  return await odp.json();
}

Kod statusu niesie informację o wyniku: 400 oznacza błąd danych wejściowych, 401 brak uwierzytelnienia, 403 brak uprawnień, a 500 błąd po stronie serwera. Rozróżnienie tych przypadków pozwala interfejsowi zareagować inaczej na każdy z nich, zamiast wyświetlać jeden ogólny komunikat.

Sesje i uwierzytelnianie w aplikacji webowej

Skoro protokół jest bezstanowy, aplikacja musi rozpoznać użytkownika przy każdym żądaniu. Najczęściej służy do tego identyfikator sesji zapisany w ciasteczku. Po poprawnym logowaniu serwer tworzy sesję, zapisuje w niej tożsamość i uprawnienia, a przeglądarka odsyła identyfikator w nagłówku Cookie przy kolejnych wywołaniach. Sesja może leżeć w pamięci serwera, w bazie albo w rozproszonej pamięci podręcznej, gdy aplikacja działa na kilku serwerach naraz. Model ten omawia dokumentacja zarządzania stanem w ASP.NET Core.

Ciasteczko sesyjne wymaga poprawnych flag. Przykładowy nagłówek odpowiedzi po logowaniu wygląda następująco:

Set-Cookie: sid=9f2c...e1; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800
AtrybutSkutek
Secureprzeglądarka wysyła ciasteczko tylko przez HTTPS
HttpOnlyskrypt JavaScript nie ma dostępu do wartości, co utrudnia jej kradzież przez wstrzyknięty kod
SameSiteogranicza wysyłanie ciasteczka w żądaniach z innych witryn, co osłabia ataki typu CSRF
Max-Ageczas życia po stronie przeglądarki, niezależny od limitu bezczynności na serwerze

Pełny zestaw zaleceń, w tym odnawianie identyfikatora po zalogowaniu i unieważnianie sesji przy wylogowaniu, zawiera arkusz OWASP o zarządzaniu sesją. Szczegóły ciasteczek opisuje dokumentacja MDN.

Logowanie w środowisku firmowym

W firmach uwierzytelnianie rzadko opiera się wyłącznie na lokalnej tabeli użytkowników. Aplikacja może przekazać weryfikację do Active Directory i wpuścić zalogowanego pracownika bez ponownego podawania hasła. Rozwiązanie takie opisuje artykuł o logowaniu i autoryzacji w programie magazynowym WMS, w którym pojawia się także logowanie jednokrotne (SSO). Uwierzytelnianie odpowiada na pytanie, kim jest użytkownik, a autoryzacja na pytanie, co wolno mu zrobić, dlatego role i uprawnienia sprawdza się na serwerze przy każdej operacji.

HTTPS i ochrona transmisji

Bez szyfrowania każdy węzeł na trasie widzi hasła oraz identyfikatory sesji. Protokół HTTPS opakowuje HTTP w TLS: serwer przedstawia certyfikat, przeglądarka weryfikuje jego łańcuch i nazwę domeny, po czym obie strony uzgadniają klucz szyfrujący sesję. Certyfikat ma ograniczony czas ważności, więc jego odnawianie należy do stałych zadań administracyjnych, a nie do jednorazowej konfiguracji.

Samo włączenie certyfikatu nie wystarcza. Serwer powinien przekierować ruch z portu 80 na HTTPS oraz wysłać nagłówek HSTS, który nakazuje przeglądarce łączyć się z domeną wyłącznie szyfrowanym kanałem. W ASP.NET Core robi się to dwoma wywołaniami w potoku żądań:

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseHsts();          // nagłówek Strict-Transport-Security
}
app.UseHttpsRedirection();  // 307 z HTTP na HTTPS
app.UseAuthentication();
app.UseAuthorization();
app.Run();

Konfigurację i zalecenia producenta platformy zawiera dokumentacja wymuszania HTTPS w ASP.NET Core. Szyfrowanie chroni kanał, ale nie dane w bazie ani w logach, więc ochrona tych miejsc wymaga osobnych decyzji.

Schemat bazy danych z tabelami, wykresami i chmurą jako źródłem danych aplikacji
Baza danych jako warstwa trwała - odpowiada za spójność zapisów, o których decyduje serwer aplikacji.

Aplikacja webowa a aplikacja desktopowa

Program desktopowy działa na stacji roboczej i korzysta z jej zasobów bezpośrednio: plików, portów szeregowych, drukarek etykiet, kart pamięci. Aplikacja webowa ma dostęp tylko do tego, co przeglądarka udostępnia przez interfejsy API. Różnica przekłada się na koszty utrzymania, ale także na zakres zadań, które da się zrealizować bez dodatkowego oprogramowania pośredniczącego.

ObszarAplikacja webowaAplikacja desktopowa
Instalacjabrak instalacji, wystarczy przeglądarkainstalator na każdej stacji
Aktualizacjajedno wdrożenie na serwerze dla wszystkichrozesłanie wersji na każdą stację
Dostęp do urządzeńograniczony do API przeglądarkipełny, przez sterowniki systemu
Praca bez sieciwymaga dodatkowych mechanizmów (pamięć lokalna, kolejka)naturalna, gdy dane są lokalne
Zależność od stacjiniska, liczy się wersja przeglądarkiwysoka, liczy się system i biblioteki

Kiedy zostaje klient desktopowy

Kilka zastosowań nadal wymaga programu instalowanego lokalnie, na przykład bezpośrednia obsługa specyficznych urządzeń albo bardzo duże zbiory danych przetwarzane na stacji. W praktyce oba modele współistnieją: Magazyn Framework jako wersja desktopowa WMS działa obok interfejsów przeglądarkowych, a firma wybiera model według stanowiska pracy. Dla pracy w ruchu dochodzi trzecia opcja, aplikacja mobilna, którą opisuje przykład awizacji dostaw w aplikacji webowej i mobilnej.

  • Stanowisko biurowe - przeglądarka wystarcza, bo wymagana jest głównie edycja dokumentów i raporty.
  • Stanowisko z urządzeniem - drukarka etykiet lub czytnik wymagają sterownika albo pośredniczącego programu.

Wydajność - pamięć podręczna i liczba zapytań

Szybkość aplikacji webowej zależy od dwóch rzeczy: ile danych przeglądarka musi pobrać oraz ile pracy serwer wykonuje przed odpowiedzią. Pierwszą część ogranicza pamięć podręczna. Pliki statyczne, czyli skrypty oraz arkusze stylów, otrzymują długi czas życia w nagłówku Cache-Control, a ich nazwy zawierają numer wersji, więc po wdrożeniu przeglądarka pobiera nowy plik zamiast starego. Odpowiedzi dynamiczne mogą nieść nagłówek ETag, dzięki któremu serwer odpowiada kodem 304 bez treści, gdy dane się nie zmieniły. Reguły cache’owania opisuje przegląd pamięci podręcznej HTTP w MDN.

ZasóbNagłówekSkutek
Skrypt z numerem wersji w nazwieCache-Control: max-age długi, immutableprzeglądarka nie odpytuje serwera o ten plik
Lista dokumentów z bazyETag i warunkowy GETodpowiedź 304 oszczędza transfer, ale serwer nadal sprawdza dane
Ekran z danymi osobowymiCache-Control: no-storeprzeglądarka nie zapisuje odpowiedzi na dysku

Liczba zapytań do bazy na jedno żądanie

Drugą część kosztu tworzą zapytania do bazy. Typowy błąd to pobranie listy nagłówków dokumentów, a następnie osobne zapytanie o pozycje każdego z nich. Dla stu dokumentów daje to sto jeden zapytań zamiast jednego. Rozwiązaniem jest jedno zapytanie ze złączeniem i indeks na kolumnie łączącej, co przy dużych tabelach zmienia czas odpowiedzi z sekund na ułamki sekundy.

-- przykład poglądowy: jedno zapytanie zamiast pętli po dokumentach
SELECT d.DokumentId, d.Numer, p.Lp, p.Ilosc
FROM   dbo.Dokument AS d
JOIN   dbo.DokumentPozycja AS p ON p.DokumentId = d.DokumentId
WHERE  d.DataWystawienia >= @Od AND d.DataWystawienia < @Do;

CREATE INDEX IX_DokumentPozycja_DokumentId
    ON dbo.DokumentPozycja (DokumentId) INCLUDE (Lp, Ilosc);

Indeks pokrywający z klauzulą INCLUDE pozwala bazie odczytać kolumny Lp i Ilosc bez sięgania do tabeli głównej. Do wykrywania najwolniejszych zapytań służy magazyn zapytań (Query Store) w SQL Server, a w aplikacji warto logować czas każdego żądania razem z liczbą wykonanych zapytań.

Dane i współbieżność w bazie

Serwer obsługuje wielu użytkowników jednocześnie, więc dwa żądania mogą zmieniać ten sam rekord w tym samym czasie. Bez zabezpieczenia wygrywa ostatni zapis i cicho kasuje cudzą zmianę. Popularnym rozwiązaniem jest blokowanie optymistyczne: rekord ma kolumnę wersji, a zapis powiedzie się tylko wtedy, gdy wersja w bazie nadal odpowiada tej, którą użytkownik pobrał.

-- przykład poglądowy, nazwy nie odwzorowują żadnego schematu produktu
ALTER TABLE dbo.Dokument ADD Wersja rowversion;

UPDATE dbo.Dokument
SET    Status = @NowyStatus
WHERE  DokumentId = @Id
  AND  Wersja = @WersjaZOdczytu;

IF @@ROWCOUNT = 0
    THROW 50001, 'Dokument zmieniony przez innego użytkownika.', 1;

Aplikacja przechwytuje ten błąd i pokazuje użytkownikowi komunikat z możliwością odświeżenia rekordu. Alternatywą jest blokowanie pesymistyczne, które wstrzymuje innych na czas edycji, ale przy pracy z przeglądarką rodzi problem porzuconych blokad: użytkownik zamyka kartę, a blokada zostaje. Zagadnienia projektowania zapytań i transakcji omawia artykuł o programowaniu baz danych.

Reguła: transakcja obejmuje jedną operację biznesową i kończy się przed zwróceniem odpowiedzi, a nie trwa razem z sesją użytkownika.

Wdrożenie i wersje aplikacji

Jedno wdrożenie na serwerze zmienia wersję dla wszystkich, co upraszcza aktualizacje, ale wymaga kontroli zgodności: skrypty w pamięci podręcznej przeglądarki mogą pochodzić ze starej wersji, a baza po migracji ma nowy schemat. Dlatego pliki statyczne oznacza się numerem wersji w adresie, a zmiany schematu wprowadza tak, by poprzednia wersja aplikacji mogła jeszcze przez chwilę z nimi współpracować. Architekturę serwerową w wariancie chmurowym opisuje System TMS online, a rozważania o wyborze modelu hostingu zawiera artykuł o wykorzystaniu chmury obliczeniowej.

Zagadnienia wytwarzania i projektowania interfejsu, których ten tekst nie rozwija, omawiają osobne materiały o tworzeniu aplikacji webowych i o projektowaniu aplikacji webowych. Oferta producenta w tym zakresie jest opisana na stronie aplikacji internetowych SoftwareStudio.

Dane wejściowe i wyjściowe jako powierzchnia ataku

Pole formularza oraz parametr adresu to dane pochodzące spoza aplikacji, więc nie wolno im ufać. Dwa najczęstsze zagrożenia wynikają z mieszania danych z kodem. Wstrzyknięcie SQL zachodzi wtedy, gdy wartość użytkownika trafia do zapytania jako tekst polecenia. Skrypty międzywitrynowe (XSS) pojawiają się, gdy wartość użytkownika trafia na stronę bez zakodowania i przeglądarka wykonuje ją jako kod.

// źle: konkatenacja tekstu użytkownika z poleceniem SQL
var sql = "SELECT * FROM Dokument WHERE Numer = '" + numer + "'";

// dobrze: parametr przekazywany osobno od polecenia
using var cmd = new SqlCommand(
    "SELECT DokumentId, Status FROM dbo.Dokument WHERE Numer = @numer", conn);
cmd.Parameters.Add("@numer", SqlDbType.NVarChar, 30).Value = numer;

Parametr jest przekazywany do bazy jako wartość, nigdy jako część polecenia, więc apostrof w numerze dokumentu nie zmienia struktury zapytania. Po stronie wyjścia obowiązuje kodowanie znaków specjalnych HTML przy każdym wstawieniu wartości do strony. Wzorce obrony opisują dokumentacja Microsoft o wstrzykiwaniu SQL oraz lista OWASP Top Ten.

Reguła: konto bazy używane przez aplikację ma uprawnienia tylko do potrzebnych tabel i procedur, a nie rolę administratora serwera.