CORS w JavaScript

Praca z api, requestami, wymianą danych z serwerem jest integralną częścią codziennej pracy frontend developera. Z tego względu postanowiłem napisać wpis o mechanizmie CORS, który podczas nauki potrafi wprowadzić w zakłopotanie.

Czym jest CORS?

CORS, czyli akronim od Cross-Origin Resource Sharing, to mechanizm działający w protokole HTTP (czyli de facto protokole, na którym działa cała sieć WWW), który odpowiada za poprawną wymianę danych pomiędzy requesterem, a więc przeglądarką (choć mogą być to również różne software pointy wykorzystujące ten protokół), a responderem. Responderem najczęściej jest jakiś serwer http serwujący strony internetowe.

Dlaczego w ogóle CORS jest istotny? Jeżeli trafiłeś tutaj poszukując informacji, jak sobie z nim poradzić, to prawdopodobnie trafiłeś na pewien error w konsoli pisząc swoje requesty do jakiegoś api. Ten error wyglądał coś na wzór tego:

zdjęcie tematyczne
każdy frontendowiec mierzył się na swojej drodzę z tym błędem

Znajome? Dlaczego się tak dzieje? Jest to mechanizm służący do zabezpieczenia api, a zarazem danych na serwerze, tak aby tylko hosty podane przez serwer mogły wymieniać dane za pomocą protokołu HTTP. Domyślasz się pewnie, że jest to dość prymitywny sposób ochrony danych. Zgadzam się z tym, bo informacje o klientach przepuszczanych przez serwer i mogących pobierać dane znajdują się w jednym z nagłówków respondera (o nagłówku tym powiemy sobie za moment), który jest w pełni jawny po stronie klienta. Ponadto na te headery (nagłówki) można wpłynąć. Jednym z takich sposobów jest wysłanie requesta curl, w którym możemy wysłać dokładnie taki origin, jaki jest przepuszczany przez serwer. Jest to więc rozwiązanie nieidealne, podatne na ataki, wykradanie danych i obciążanie serwera. Dlaczego jest więc stosowane? Wprawdzie nie jest idealne, ale dość skutecznie zapobiega i ogranicza niezidentyfikowanej wymiany danych w sieci. Można powiedzieć, że jest to pierwszy krok do bezpieczeństwa aplikacji webowych, a tym samym, frontend developerzy muszą się z nimi mierzyć. Po większe bezpieczeństwo w kontekście komunikacji z backendem, korzysta się z autoryzacji wraz z cookies służących do autoryacji użytkowników, którzy mogą pobierać dane, ale o tym kiedy indziej.

Jak sobie radzić z CORSami?

Informacja o hoście bądź nazwie hosta, z którego wysyłane jest żądanie, przekazywana jest w headerze Origin protokołu HTTP. Możemy to łatwo sprawdzić wchodząc w devToolsy przeglądarki i w zakładce network kliknąć pierwszy lepszy resource (zasób sieciowy), który, wchodząc na stronę, otrzymaliśmy:

zdjęcie tematyczne

Po stronie serwera header odpowiedzialny za przepuszczanie danych hostów to Access-Control-Allow-Headers, znany bardzo dobrze z konsolowego errora otrzymanego wyżej:

zdjęcie tematyczne

Jak można zauważyć, oba headery nie są równe sobie - oznacza to, że do pobrania żądanych danych nie dojdzie (cokolwiek by to było), a my możemy się zadowolić co najwyżej pięknym czerwonym klockiem, frustrując się równie czerwono. Nie no żartuję, aż tak źle nie będzie. Co więc możemy w takiej sytuacji zrobić? Szczerze mówiąc, nie dużo. Kontrola przepływu danych klient - api w znacznym stopniu zależy od backendowca odpowiedzialnego za kod serwera, z którym chcemy się skomunikować. To backend ustala jakie hosty przejdą. Podczas developmentu aplikacji najczęściej bywa tak, że to wszystkie hosty wewnątrz sieci firmowej lub w ogóle wszystkie hosty są przepuszczane. Na wersji produkcyjnej serwera jest to najczęściej ta jedna domena, na której stoi aplikacja frontendowa i która musi mieć dostęp do api. Oczywiście w publicznych api, takich jak PokéAPI, czy punkAPI (polecam do nauki fetchowania). W przeciwnym wypadku musimy poprosić backendowca o dodanie naszego hosta do listy hostów mogących się komunikować z api. Ten błąd więc nie oznacza, że to z naszej winy jest coś nie tak (pod warunkiem, że request został wykonany poprawnie). I na tym tak naprawdę mógłbym zakończyć ten wpis, ale pokuszę się o coś więcej.

Prosty serwer node w oparciu o Express.js

Tak, dobrze myślisz, widząc ten nagłówek. Napiszemy swój własny, bądź co bądź bardzo prosty serwer http. Wykorzystamy do tego node, a ponadto oparty na nim framework Express.js. Ktoś może się spytać, dlaczego pojawia się tutaj node skoro blog jest o frontendzie. Owszem, moim głównym obiektem zainteresowania jest frontend, ale w codziennej pracy programisty webowego ważne jest, żeby znać zasadę działania nie tylko frontu, ale też rozwiązań, które z nim współpracują. Ponadto node to nasz kochany JavaScript, więc nie będzie tutaj mowy o nieznajomości składni. Przejdźmy więc zatem do pracy. 

Pierwsze co zrobimy to stworzenie workspace do frontu. Zwykły index.js podpięty do index.html powinien wystarczyć:

const BASE_URL = 'http://localhost:3000';

const fetchApi = async () => {
  const fetchedResponse = await fetch(BASE_URL, { cache: 'no-cache' })
    .then((response) => response.json())
    .then((data) => data)
    .catch((error) => console.log(error));

  fetchedResponse ? console.log(fetchedResponse) : null;
};

console.log('asd');

fetchApi();

Będziemy tu fetchować dane z naszego przyszłego serwera, działającego na porcie 3000. Po sukcesywnym otrzymaniu danych z api lub niepowodzeniu, w konsoli wyświetlimy odpowiednio otrzymane dane lub komunikat errora. Stwórzmy workspace dla naszego serwera, w nim plik index.js a w nim umieśćmy dany kod:

const express = require('express');   // załadowanie dependencji express
const app = express();  // zainicjowanie obiektu app, który jest odpowiedzialny za wystawianie endpointów

app.get('/', (req, res) => {  // wystawienie endpointa GET podanego na bazowym url
  res.json('Hello World!');   // odpowiedź serwera w postaci klucza json, który odbieramy na froncie
});

app.listen(3000, function () {  // uruchomienie serwera
  console.log('Listening on port 3000');  // wiadomość w konsoli po uruchomieniu
});

Aby powyższy kod zadziałał, potrzebujemy oczywiście node.js (który zapewnie posiadasz na swoim komputerze) i paczkę od expressa. Zainstalujmy więc ją:

npm install express

Po instalacji expressa, możemy uruchomić serwer za pomocą:

node index.js

Po uruchomieniu aplikacja będzie działać na porcie 3000. Możesz zobaczyć, co otrzymamy wchodząc na http://localhost:3000. Tym samym zaserwowaliśmy w pełni działający serwer http, który zwraca dane na zapytanie GET. Spróbujmy jednak zaciągnąć te dane za pomocą wcześniej napisanej aplikacji frontowej. Jak zapewne się domyślasz, na przeszkodzie staną nam nieszczęsne CORSy, z którymi będziemy musieli sobie poradzić.

Do tego celu potrzebna nam będzie dodatkowa biblioteka wykorzystująca node, a mianowicie cors. Zainstalujemy ją:

npm install cors

I zmodyfikujemy nieco kod:

const express = require('express');
const app = express();
const cors = require('cors');

app.use(cors());  // identyfikujemy główny obiekt z mechanizmem cors

app.get('/', cors(), (req, res) => {  // dodajemy middleware do endpointa
  res.json('Hello World!');
});

app.listen(3000, function () {
  console.log('Listening on port 3000');
});

Dzięki temu wszystkie źródła i hosty będą przepuszczane przez header (taki jest domyślny sposób działania funkcji cors()). Pozbyliśmy się więc problemu. I ponownie mógłbym tutaj zakończyć mój wywód, ale pójdźmy o krok dalej. Przepuśćmy tylko i wyłącznie nasz origin:

const express = require('express');
const app = express();
const cors = require('cors');

app.use(cors());

const config = {
  // konfiguracja corsów
  origin: 'http://127.0.0.1:1234', // adres ip 127.0.0.1 to odpowiednik localhost
};

app.get('/', cors(config), (req, res) => {
  // wysłanie configu jako argument funkcji cors()
  res.status(219).json('Hello World!');
});

app.listen(3000, function () {
  console.log('Listening on port 3000');
});

Dzięki temu teoretycznie żaden inny host nie powinien wyciągnąć danych z tego serwera. Zauważ że dzięki expressowi, możemy również manipulować kodami odpowiedzi protokołu HTTP, co jest niesamowite samo w sobie.

Podsumowanie

Jak już wspomniałem, praca z backendem i api to integralna i chyba najważniejsza część pracy frontendowca. Dobry programista interfejsów musi umieć komunikować się z niewizualną częścią aplikacji w sposób biegły. Ponieważ to backend w połączeniu frontendem stają się aplikacją kompletną, profesjonalną, biznesową itd. Poniekąd cały Internet działa na tym, co tutaj opisałem. Ponadto dowiedziałeś się, na czym polega mechanizm CORS, jak sobie z nim radzić (są to bardzo często zadawane pytania w procesach rekrutacyjnych), a nawet, z czym mierzą się backend developerzy. Oczywiście jest to kropla w morzu i poruszyłem tu zupełne podstawy backendu, to mam jednak nadzieję, ze choć trochę zrozumiałeś jego koncepcję.

© Damian Kalka 2021
Wszelkie prawa zastrzeżone