Ewolucja architektury webowej: wspomnienie PHP i koszmar CORS

„Wtedy wystarczał jeden plik…”

PHP, z którym pracowałem na studiach, było naprawdę wygodne. W jednym pliku index.php można było umieścić znaczniki HTML, otworzyć pomiędzy nimi <?php ?> i dopisać tam kod odpytywania bazy danych. Nie było wyraźnego podziału na frontend i backend. Wystarczyło zadeklarować zmienną, by używać jej w dowolnym miejscu w HTML-u, a błędy związane z komunikacją danych praktycznie się nie zdarzały.

Ale kiedy w pracy przesiadłem się na stos technologiczny z Vue.js po stronie frontendu i Spring Boot po stronie backendu, zaczęło się piekło.

„Przecież uruchomiłem lokalnie serwer Springa, ekran Vue też działa, więc dlaczego dane nie przychodzą?”

Konsola w narzędziach deweloperskich Chrome była cała zalana czerwonymi komunikatami o błędach.

Access to XMLHttpRequest at ... has been blocked by CORS policy

Czym właściwie jest ten cały CORS, skoro blokuje moje dane? I dlaczego porzuciliśmy ten wygodny dawny sposób, w którym jeden plik wystarczał do wszystkiego, tylko po to, żeby specjalnie rozdzielić frontend od backendu?

Kiedyś mieszkali pod jednym dachem, a dziś żyją w osobnych domach i muszą przechodzić przez kontrolę.

Kto ma gotować? (SSR vs CSR)

Żeby zrozumieć tę zmianę, trzeba najpierw zrozumieć, kto przygotowuje „potrawę” zwaną stroną internetową, czyli HTML. To właśnie na tym polega różnica między SSR (Server Side Rendering) a CSR (Client Side Rendering).

1. SSR: gotowy lunchbox (PHP, JSP)

  • Sposób działania: serwer (kuchnia) przygotowuje ryż, dodatki i zupę, ładnie pakuje wszystko do lunchboxa (HTML) i wysyła.
  • Klient (przeglądarka): wystarczy, że zdejmie pokrywkę lunchboxa i pokaże zawartość.
  • Zalety: pierwszy ekran ładuje się szybko. Wyszukiwarki (SEO) łatwo odczytują treść.
  • Wady: nawet żeby zmienić jeden dodatek, trzeba od nowa odebrać cały lunchbox. (Migotanie ekranu)

2. CSR: dostarczony meal kit (React, Vue)

  • Sposób działania: serwer nie gotuje. Wysyła tylko składniki (dane JSON) i przepis (JavaScript).
  • Klient (przeglądarka): z otrzymanych składników sam przyrządza danie na moim komputerze (w przeglądarce) i w ten sposób buduje ekran.
  • Zalety: można podmieniać pojedyncze dodatki, więc przejścia między ekranami są bardzo płynne (jak w aplikacji). Spada obciążenie serwera.
  • Wady: na początku trzeba pobrać przepis (pliki JS), więc pierwsze ładowanie jest wolne.

[Tip] CSR i SPA nie znaczą dokładnie tego samego

Często używa się tych terminów zamiennie, ale technicznie nie są one tym samym.

  • CSR (Client Side Rendering): to „sposób (How)”. Oznacza metodę, w której ekran rysuje przeglądarka.
  • SPA (Single Page Application): to „forma (What)”. Oznacza aplikację, która ma tylko jedną stronę.

Zwykle używamy CSR po to, żeby zbudować SPA. Innymi słowy, ponieważ chcemy uniknąć migotania związanego z pełnym przeładowaniem strony dzięki jednej stronie (SPA), wybieramy podejście, w którym przeglądarka sama przerysowuje potrzebne fragmenty (CSR).

Powodem, dla którego używamy Vue.js albo Reacta, jest doświadczenie użytkownika. Żeby strona działała płynnie jak aplikacja mobilna, przenieśliśmy „gotowanie” z serwera do przeglądarki.

CORS, ochroniarz blokujący nieproszonych gości

Po rozdzieleniu frontendu (Vue) i backendu (Spring) w praktyce dostaliśmy „dwa domy”.

  • Dom frontendu: localhost:8080 (serwer Vue)
  • Dom backendu: localhost:3000 (serwer Spring)

I właśnie tutaj pojawia się problem. Ze względów bezpieczeństwa przeglądarka kieruje się podstawową zasadą: „nie ufaj zbyt łatwo zasobom pochodzącym z innego Origin”. To właśnie SOP, czyli Same Origin Policy.

Wyobraź sobie taką sytuację. Jestem zalogowany do Naver, a ze strony stworzonej przez hakera po cichu wysyłane jest żądanie do serwera Naver: „daj mi prywatne dane tego użytkownika”. Gdyby przeglądarka tego nie zablokowała, wszystkie moje informacje byłyby wystawione na widok.

Dlatego przeglądarka domyślnie blokuje żądania, kiedy origin, czyli domena albo port, jest inny. A my dla potrzeb developmentu właśnie celowo rozdzieliliśmy porty. Chcemy tylko używać własnych danych, a przeglądarka, niczym ochroniarz, mówi: „Twój port się nie zgadza. Wstęp wzbroniony.” Taka jest prawdziwa natura błędu CORS, czyli Cross-Origin Resource Sharing.

[Code Verification] Wystawienie przepustki

Sposób rozwiązania tego problemu jest w gruncie rzeczy prosty. Serwer, czyli backend, ma po prostu napisać przeglądarce, ochroniarzowi: „to gość, którego sam zaprosiłem, proszę go wpuścić”.

W Spring Boot taką przepustkę można wystawić za pomocą jednego pliku konfiguracyjnego.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 1. Dla wszystkich sciezek API
                .allowedOrigins("http://localhost:8080") // 2. Zezwalaj na zadania z tego adresu
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 3. Zezwalaj na te metody zadan
                .allowCredentials(true); // 4. Mozna dolaczyc ciasteczka i dane uwierzytelniajace
    }
}

Analiza:

  1. allowedOrigins: najważniejsze ustawienie. Jeśli wpiszesz tu adres serwera frontendu (localhost:8080), serwer Springa przybije w nagłówku odpowiedzi pieczątkę Access-Control-Allow-Origin: http://localhost:8080 i dopiero wtedy ją wyśle.
  2. Przeglądarka sprawdza tę pieczątkę, stwierdza: „Aha, właściciel serwera na to pozwolił”, i przekazuje dane do frontendu.

Trzeba jednak uważać, żeby z lenistwa nie otworzyć wszystkiego przez allowedOrigins("*"). To byłoby jak zostawienie otwartych na oścież drzwi wejściowych z tabliczką „złodzieje mile widziani”.

Praktyczna rada: kiedy czego używać?

Czy to znaczy, że CSR, czyli Vue albo React, jest zawsze właściwą odpowiedzią? Nie. W praktyce trzeba wybierać zależnie od celu.

  1. Panel administracyjny (Admin), dashboardy:
    • Rekomendacja: CSR (Vue, React)
    • Powód: dużo interakcji z danymi, a widoczność w wyszukiwarkach nie jest potrzebna. Liczy się żwawa, responsywna reakcja jak w aplikacji.
  2. Serwisy informacyjne, blogi, strony główne sklepów internetowych:
    • Rekomendacja: SSR (Next.js, Nuxt.js)
    • Powód: strona musi dobrze wypadać w wyszukiwarce Google (SEO), a gdy klient wejdzie, zamiast białego ekranu od razu powinna pojawić się treść.
  3. Małe projekty prywatne:
    • Rekomendacja: Thymeleaf (SSR wbudowany w Spring Boot)
    • Powód: skoro rozwijasz projekt sam, nie ma sensu męczyć się z rozdzielaniem frontendu i backendu ani z konfiguracją CORS. Rozwijanie wszystkiego w jednym miejscu, jak w czasach PHP, daje najwyższą produktywność.

Na zakończenie: poza granicą

Dawna „wielka rodzina pod jednym dachem” z czasów PHP była wygodna, ale im bardziej wszystko się komplikowało, tym trudniej było tym zarządzać. Dzisiejszy model „dwóch rozdzielonych rodzin”, frontendu i backendu, jest bardziej uciążliwy przez komunikację i CORS, ale pozwala każdej stronie skupić się na swojej roli.

Więc następnym razem, gdy trafisz na błąd CORS, spróbuj nie reagować wyłącznie z irytacją. Przeglądarka cię nie gnębi. Po prostu przeprowadza rygorystyczną kontrolę, żeby chronić twój dom, czyli twój serwer.

Teraz przejście między frontendem a backendem jest już otwarte. Dane mogą przepływać w obie strony. Ale gdzie właściwie te dane są przechowywane po stronie serwera? Kiedyś wystarczyło rzucić zapytanie i pobrać to, czego się potrzebowało. W Springu, przez takie rzeczy jak JPA czy Entity, ma się czasem wrażenie, że nie wolno już pisać SQL bezpośrednio.

Następnym razem zobaczymy, jak „podręcznikowy” sposób projektowania baz danych, czyli normalizacja, potrafi w praktyce stać się przeszkodą, oraz dlaczego JPA tak bardzo próbuje ukryć przed nami SQL.

Dodaj komentarz