Fetch:Cross-Origin Requests

Pokud odešleme fetch požadavek na jiný web, pravděpodobně selže.

Zkusme například načíst http://example.com :

try {
  await fetch('http://example.com');
} catch(err) {
  alert(err); // Failed to fetch
}

Načítání se nezdařilo, jak se očekávalo.

Základním konceptem je zde původ – triplet doména/port/protokol.

Cross-origin požadavky – ty odeslané do jiné domény (dokonce i subdomény) nebo protokolu či portu – vyžadují speciální hlavičky ze vzdálené strany.

Tato zásada se nazývá „CORS“:Cross-Origin Resource Sharing.

Proč je potřeba CORS? Stručná historie

CORS existuje, aby chránil internet před zlými hackery.

Vážně. Udělejme velmi krátkou historickou odbočku.

Po mnoho let neměl skript z jednoho webu přístup k obsahu jiného webu.

Toto jednoduché, ale účinné pravidlo bylo základem internetové bezpečnosti. Např. zlý skript z webu hacker.com nemohl získat přístup k poštovní schránce uživatele na webu gmail.com . Lidé se cítili bezpečně.

JavaScript také v té době neměl žádné speciální metody pro provádění síťových požadavků. Byl to hračkářský jazyk k ozdobení webové stránky.

Weboví vývojáři však požadovali více výkonu. Byla vynalezena řada triků, jak obejít omezení a odeslat požadavky na jiné webové stránky.

Používání formulářů

Jedním ze způsobů, jak komunikovat s jiným serverem, bylo odeslat <form> tam. Lidé jej odeslali do <iframe> , stačí zůstat na aktuální stránce, například takto:

<!-- form target -->
<iframe name="iframe"></iframe>

<!-- a form could be dynamically generated and submited by JavaScript -->
<form target="iframe" method="POST" action="http://another.com/…">
  ...
</form>

Bylo tedy možné provést požadavek GET/POST na jiný web, a to i bez síťových metod, protože formuláře mohou odesílat data kamkoli. Ale protože je zakázán přístup k obsahu <iframe> z jiného webu, nebylo možné přečíst odpověď.

Abych byl přesný, byly na to vlastně triky, vyžadovaly speciální skripty jak na iframe, tak na stránce. Takže komunikace s iframe byla technicky možná. Právě teď nemá smysl zabíhat do podrobností, nechte tyto dinosaury odpočívat v pokoji.

Používání skriptů

Dalším trikem bylo použití script štítek. Skript může mít libovolný src , s libovolnou doménou, například <script src="http://another.com/…"> . Skript je možné spustit z libovolného webu.

Pokud web, např. another.com zamýšlel vystavit data pro tento druh přístupu, pak byl použit takzvaný protokol „JSONP (JSON with padding)“.

Zde je návod, jak to fungovalo.

Řekněme, že na našem webu potřebujeme získat data z http://another.com , jako je počasí:

  1. Nejprve předem deklarujeme globální funkci pro akceptování dat, např. gotWeather .

    // 1. Declare the function to process the weather data
    function gotWeather({ temperature, humidity }) {
      alert(`temperature: ${temperature}, humidity: ${humidity}`);
    }
  2. Poté vytvoříme <script> tag s src="http://another.com/weather.json?callback=gotWeather" , s použitím názvu naší funkce jako callback URL-parametr.

    let script = document.createElement('script');
    script.src = `http://another.com/weather.json?callback=gotWeather`;
    document.body.append(script);
  3. Vzdálený server another.com dynamicky generuje skript, který volá gotWeather(...) s údaji, které chceme, abychom obdrželi.

    // The expected answer from the server looks like this:
    gotWeather({
      temperature: 25,
      humidity: 78
    });
  4. Když se vzdálený skript načte a spustí, gotWeather běží, a protože je to naše funkce, máme data.

To funguje a nenarušuje bezpečnost, protože obě strany souhlasily s předáváním dat tímto způsobem. A když se obě strany dohodnou, rozhodně to není hack. Stále existují služby, které takový přístup poskytují, protože funguje i pro velmi staré prohlížeče.

Po chvíli se v JavaScriptu prohlížeče objevily síťové metody.

Zpočátku byly žádosti o cross-origin zakázány. Ale v důsledku dlouhých diskusí byly povoleny požadavky napříč původem, ale s jakýmikoli novými možnostmi vyžadujícími explicitní povolení ze strany serveru, vyjádřené ve speciálních hlavičkách.

Bezpečné požadavky

Existují dva typy požadavků napříč původem:

  1. Bezpečné požadavky.
  2. Všechny ostatní.

Bezpečné požadavky jsou jednodušší na vytváření, začněme jimi.

Požadavek je bezpečný, pokud splňuje dvě podmínky:

  1. Bezpečná metoda:GET, POST nebo HEAD
  2. Bezpečná záhlaví – jediná povolená vlastní záhlaví jsou:
    • Accept ,
    • Accept-Language ,
    • Content-Language ,
    • Content-Type s hodnotou application/x-www-form-urlencoded , multipart/form-data nebo text/plain .

Jakýkoli jiný požadavek je považován za „nebezpečný“. Například požadavek s PUT nebo pomocí API-Key HTTP hlavička nesplňuje omezení.

Zásadní rozdíl je v tom, že bezpečný požadavek lze provést pomocí <form> nebo <script> , bez jakýchkoli speciálních metod.

Takže i velmi starý server by měl být připraven přijmout bezpečný požadavek.

Naproti tomu požadavky s nestandardními hlavičkami nebo např. metoda DELETE nelze tímto způsobem vytvořit. Po dlouhou dobu nebyl JavaScript schopen takové požadavky provádět. Takže starý server může předpokládat, že takové požadavky pocházejí z privilegovaného zdroje, „protože je webová stránka nemůže odeslat“.

Když se pokusíme vytvořit nebezpečný požadavek, prohlížeč odešle speciální požadavek „před výstupem“, který se serveru zeptá – souhlasí s přijetím takových požadavků z různých zdrojů, nebo ne?

A pokud server výslovně nepotvrdí, že s hlavičkami, není nebezpečný požadavek odeslán.

Nyní půjdeme do podrobností.

CORS pro bezpečné požadavky

Pokud je požadavek cross-origin, prohlížeč vždy přidá Origin záhlaví.

Pokud například požadujeme https://anywhere.com/request od https://javascript.info/page , budou záhlaví vypadat takto:

GET /request
Host: anywhere.com
Origin: https://javascript.info
...

Jak můžete vidět, Origin hlavička obsahuje přesně původ (doménu/protokol/port), bez cesty.

Server může zkontrolovat Origin a pokud souhlasí s přijetím takového požadavku, přidá speciální hlavičku Access-Control-Allow-Origin na odpověď. Tato hlavička by měla obsahovat povolený původ (v našem případě https://javascript.info ), nebo hvězdička * . Pak je odpověď úspěšná, jinak je to chyba.

Prohlížeč zde hraje roli důvěryhodného prostředníka:

  1. Zajišťuje správnou hodnotu Origin je odeslána s požadavkem na křížový původ.
  2. Zkontroluje povolení Access-Control-Allow-Origin v odpovědi, pokud existuje, má JavaScript povolen přístup k odpovědi, jinak selže s chybou.

Zde je příklad permisivní odpovědi serveru:

200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info

Záhlaví odpovědí

U požadavku mezi původem může JavaScript ve výchozím nastavení přistupovat pouze k takzvaným „bezpečným“ hlavičkám odpovědí:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

Přístup k jakékoli jiné hlavičce odpovědi způsobí chybu.

Poznámka:

Neexistuje žádné Content-Length záhlaví v seznamu!

Tato hlavička obsahuje celou délku odpovědi. Pokud tedy něco stahujeme a chtěli bychom sledovat procento pokroku, pak je pro přístup k této hlavičce vyžadováno další oprávnění (viz níže).

Chcete-li udělit přístup JavaScript jakékoli jiné hlavičce odpovědi, musí server odeslat Access-Control-Expose-Headers záhlaví. Obsahuje čárkami oddělený seznam nebezpečných názvů hlaviček, které by měly být zpřístupněny.

Například:

200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Length,API-Key

S takovým Access-Control-Expose-Headers záhlaví, skript smí číst Content-Length a API-Key záhlaví odpovědi.

„Nebezpečné“ požadavky

Můžeme použít jakoukoli metodu HTTP:nejen GET/POST , ale také PATCH , DELETE a další.

Před časem si nikdo nedokázal ani představit, že by webová stránka mohla klást takové požadavky. Stále tedy mohou existovat webové služby, které považují nestandardní metodu za signál:„To není prohlížeč“. Mohou to vzít v úvahu při kontrole přístupových práv.

Aby nedocházelo k nedorozuměním, jakýkoli „nebezpečný“ požadavek – který za starých časů nebylo možné provést, prohlížeč takové požadavky nedává hned. Nejprve odešle předběžnou, takzvanou „předletovou“ žádost o povolení.

Předtiskový požadavek používá metodu OPTIONS , žádné tělo a tři záhlaví:

  • Access-Control-Request-Method hlavička má metodu nebezpečného požadavku.
  • Access-Control-Request-Headers hlavička poskytuje seznam nebezpečných HTTP hlaviček oddělených čárkami.
  • Origin hlavička říká, odkud požadavek přišel. (například https://javascript.info )

Pokud server souhlasí se zpracováním požadavků, měl by odpovědět prázdným tělem, stavem 200 a záhlavími:

  • Access-Control-Allow-Origin musí být buď * nebo žádající původ, například https://javascript.info , abyste to povolili.
  • Access-Control-Allow-Methods musí mít povolenou metodu.
  • Access-Control-Allow-Headers musí mít seznam povolených záhlaví.
  • Navíc záhlaví Access-Control-Max-Age může zadat počet sekund pro uložení oprávnění do mezipaměti. Prohlížeč tedy nebude muset posílat předběžnou kontrolu pro následné požadavky, které splňují daná oprávnění.

Podívejme se, jak to funguje, krok za krokem na příkladu cross-origin PATCH požadavek (tato metoda se často používá k aktualizaci dat):

let response = await fetch('https://site.com/service.json', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
    'API-Key': 'secret'
  }
});

Existují tři důvody, proč není požadavek bezpečný (stačí jeden):

  • Metoda PATCH
  • Content-Type není jedno z:application/x-www-form-urlencoded , multipart/form-data , text/plain .
  • „Nebezpečné“ API-Key záhlaví.

Krok 1 (požadavek před výstupem)

Před odesláním takového požadavku prohlížeč sám odešle požadavek před výstupem, který vypadá takto:

OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key
  • Metoda:OPTIONS .
  • Cesta – přesně stejná jako hlavní požadavek:/service.json .
  • Speciální záhlaví napříč původy:
    • Origin – zdroj původu.
    • Access-Control-Request-Method – požadovaná metoda.
    • Access-Control-Request-Headers – seznam „nebezpečných“ záhlaví oddělených čárkami.

Krok 2 (odpověď před výstupem)

Server by měl odpovědět stavem 200 a hlavičkami:

  • Access-Control-Allow-Origin: https://javascript.info
  • Access-Control-Allow-Methods: PATCH
  • Access-Control-Allow-Headers: Content-Type,API-Key .

To umožňuje budoucí komunikaci, jinak dojde k chybě.

Pokud server očekává v budoucnu další metody a hlavičky, má smysl je povolit předem přidáním do seznamu.

Tato odpověď například také umožňuje PUT , DELETE a další záhlaví:

200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400

Nyní prohlížeč vidí, že PATCH je v Access-Control-Allow-Methods a Content-Type,API-Key jsou v seznamu Access-Control-Allow-Headers , takže odešle hlavní požadavek.

Pokud je tam záhlaví Access-Control-Max-Age s počtem sekund, pak jsou oprávnění pro kontrolu před výstupem uložena do mezipaměti pro daný čas. Výše uvedená odpověď bude uložena v mezipaměti po dobu 86 400 sekund (jeden den). V tomto časovém rámci následné požadavky nezpůsobí předletovou kontrolu. Za předpokladu, že vyhovují povolenkám uloženým v mezipaměti, budou odeslány přímo.

Krok 3 (skutečný požadavek)

Když je kontrola před výstupem úspěšná, prohlížeč nyní zadá hlavní požadavek. Postup je zde stejný jako u bezpečných požadavků.

Hlavní požadavek má Origin záhlaví (protože jde o křížový původ):

PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascript.info

Krok 4 (skutečná odpověď)

Server by neměl zapomenout přidat Access-Control-Allow-Origin na hlavní odpověď. Úspěšná příprava před výstupem z toho neuleví:

Access-Control-Allow-Origin: https://javascript.info

Potom je JavaScript schopen číst odpověď hlavního serveru.

Poznámka:

Požadavek na kontrolu před výstupem probíhá „za scénou“, je neviditelný pro JavaScript.

JavaScript obdrží odpověď na hlavní požadavek nebo chybu pouze v případě, že neexistuje žádné oprávnění serveru.

Přihlašovací údaje

Požadavek napříč původem iniciovaný kódem JavaScript ve výchozím nastavení nepřináší žádné přihlašovací údaje (soubory cookie nebo ověřování HTTP).

To je u požadavků HTTP neobvyklé. Obvykle požadavek na http://site.com je doprovázeno všemi soubory cookie z této domény. Na druhou stranu požadavky cross-origin provedené metodami JavaScriptu jsou výjimkou.

Například fetch('http://another.com') neposílá žádné cookies, ani ty (!), které patří another.com domény.

Proč?

Je to proto, že žádost s přihlašovacími údaji je mnohem výkonnější než bez nich. Pokud je to povoleno, uděluje JavaScriptu plnou pravomoc jednat jménem uživatele a přistupovat k citlivým informacím pomocí jeho přihlašovacích údajů.

Opravdu server tolik důvěřuje skriptu? Poté musí explicitně povolit požadavky s přihlašovacími údaji s další hlavičkou.

Chcete-li odeslat přihlašovací údaje v fetch , musíme přidat volbu credentials: "include" , takto:

fetch('http://another.com', {
  credentials: "include"
});

Nyní fetch odesílá soubory cookie pocházející z another.com s požadavkem na daný web.

Pokud server souhlasí s přijetím požadavku s přihlašovacími údaji , měl by přidat záhlaví Access-Control-Allow-Credentials: true na odpověď, kromě Access-Control-Allow-Origin .

Například:

200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true

Poznámka:Access-Control-Allow-Origin je zakázáno používat hvězdičku * pro žádosti s přihlašovacími údaji. Jak je uvedeno výše, musí tam uvádět přesný původ. To je další bezpečnostní opatření, které zajistí, že server skutečně ví, komu důvěřuje, že takové požadavky podává.

Shrnutí

Z pohledu prohlížeče existují dva druhy požadavků z různých zdrojů:„bezpečné“ a všechny ostatní.

„Bezpečné“ požadavky musí splňovat následující podmínky:

  • Metoda:GET, POST nebo HEAD.
  • Záhlaví – můžeme nastavit pouze:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type na hodnotu application/x-www-form-urlencoded , multipart/form-data nebo text/plain .

Zásadní rozdíl je v tom, že bezpečné požadavky byly proveditelné od starověku pomocí <form> nebo <script> tagy, zatímco nebezpečné byly pro prohlížeče po dlouhou dobu nemožné.

Praktický rozdíl je tedy v tom, že bezpečné požadavky jsou odesílány okamžitě s Origin záhlaví, zatímco u ostatních prohlížeč předběžně požádá o „preflight“ žádost o povolení.

Pro bezpečné požadavky:

  • → Prohlížeč odešle Origin záhlaví s původem.
  • ← Pro požadavky bez přihlašovacích údajů (ve výchozím nastavení se neodesílají) by měl server nastavit:
    • Access-Control-Allow-Origin na * nebo stejnou hodnotu jako Origin
  • ← Pro požadavky s přihlašovacími údaji by měl server nastavit:
    • Access-Control-Allow-Origin na stejnou hodnotu jako Origin
    • Access-Control-Allow-Credentials na true

Navíc udělit JavaScript přístup ke všem hlavičkám odpovědí kromě Cache-Control , Content-Language , Content-Type , Expires , Last-Modified nebo Pragma , server by měl ty povolené vypsat v Access-Control-Expose-Headers záhlaví.

V případě nebezpečných požadavků je před požadovaným vydán předběžný požadavek „před výstupem“:

  • → Prohlížeč odešle OPTIONS požadavek na stejnou adresu URL se záhlavím:
    • Access-Control-Request-Method má požadovanou metodu.
    • Access-Control-Request-Headers uvádí nebezpečná požadovaná záhlaví.
  • ← Server by měl odpovědět stavem 200 a hlavičkami:
    • Access-Control-Allow-Methods se seznamem povolených metod,
    • Access-Control-Allow-Headers se seznamem povolených záhlaví,
    • Access-Control-Max-Age s počtem sekund na uložení oprávnění do mezipaměti.
  • Potom se odešle skutečný požadavek a použije se předchozí „bezpečné“ schéma.