Le nuove tendenze per lo sviluppo di applicazioni web, prevedono di separare l'interfaccia utente e la logica applicativa/accesso ai dati, in due o più applicazioni.

Ad esempio l'interfaccia utente è su un dominio, supponiamo static.sgart.it, e contiene solo file HTML, CSS, JavaScript, immagini... insomma solo file statici tipicamente realizzata con dei framework come Angular o React.

La parte di logica di business e di accesso ai dati è demandata ad un altra applicazione (o più anche distribuite nel mondo), ad esempio api.sgart.it che espongono delle API JSON che vengono consumate dalla parte statica.
Le due parti potrebbero essere ospitate su Azure usando per la parte statica le Static Web Apps e per le API le Functions.
Schema logico APP
Schema logico APP

Cross-Origin Resource Sharing (CORS)

Qui sorge un problema, se da una pagina HTML del domino static.sgart.it cerchiamo di fare una chiamata AJAX usando fetch verso api.sgart.it

JavaScript

fetch("https://api.sgart.it", {
    method: "POST",
    headers: { "Content-Type": "application/json"  },
  })
    .then((response) => response.json())
    .then((data) => { ... })
    .catch((error) => { ... });
otteniamo l'errore
TypeError: NetworkError when attempting to fetch resource.
CORS Allow Origin Not Matching Origin
NS_ERROR_DOM_BAD_URI
Questa è una restrizione di sicurezza del browser.

Per funzionare è necessario che il server, api.sgart.it, ritorni nell'header delle specifiche intestazioni (headers) che autorizzino il browser ad effettuare la chiamata dal dominio statis.sgart.it.

Le intestazioni sono queste:

Text: CORS Headers

Access-Control-Allow-Origin: http://static.sgart.it
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Allow-Methods: OPTIONS,GET,POST
dove
  • Access-Control-Allow-Origin: specifica il dominio che verrà autorizzare a contattare il server dal browser
  • Access-Control-Allow-Headers: definisce quali header sono accettati nella richiesta
  • Access-Control-Allow-Methods: definisce quali metodi HTTP sono permessi
L'intestazione importante è http://static.sgart.it:8080 che deve coincidere con il dominio del sito statico senza nessuna slash finale, ad esempio http://static.sgart.it:8080/ non va bene.
Per semplicità, per la demo, ho utilizzato solo il protocollo http.
Nella realtà i produzione, per la massima sicurezza, andrà utilizzato solo il protocollo https sia per risorse interne all'azienda sia per i siti pubblici.

Quando il browser deve eseguire una chiamata AJAX su un altro dominio, la prima volta nella sessione, come prima cosa esegue una chiamata HTTP di tipo OPTIONS (preflight request), se il server risponde con le intestazioni corrette prosegue con la normale chiamata
Il browser, nella richiesta, aggiunge sempre l'header Origin con la url del dominio del sito corrente (senza slash finale)
Developer toolbar OPTIONS
Developer toolbar OPTIONS
Developer toolbar POST
Developer toolbar POST

Soluzione

Per superare l'ostacolo dobbiamo modificare il server (API) e il client (static) per gestire le chiamate CORS

Server

Il server è sviluppato in NodeJS in JavaScript ed è volutamente semplice solo per dimostrare l'uso dei metodi HTTP e delle intestazioni CORS

JavaScript: server.js

const http = require('http');
const fs = require('fs');
const path = require('path');

const port = process.env.PORT || '3000';

http.createServer(function (request, response) {
    console.log('request starting ' + request.url + '...');	//visualizzo nella console
 
    const headers = {
        'Access-Control-Allow-Origin': 'http://static.sgart.it:8080', // non mettere la slash finale
        'Access-Control-Allow-Headers': 'Content-Type,Authorization',
        'Access-Control-Allow-Methods': 'OPTIONS,GET,POST',
        'Access-Control-Max-Age': 86400, // 1 days * 24 ore * 60 minuti * 60 secondi
        'Vary': 'Origin, Access-Control-Request-Headers, Access-Control-Request-Method'
        /* altri header se necessario */
    };

    console.log('---------------------------------');
    console.log(`Request [${request.method}] ${new Date()}`, request.headers);

    if (request.method === 'OPTIONS') {
        console.log('preflight request');

        response.writeHead(204, headers);
        response.end();
        return;
    }

    if (['GET', 'POST'].indexOf(request.method) > -1) {
        const content = { date: new Date() };
        console.log('content', content);

        response.writeHead(200, headers);
        response.end(JSON.stringify(content), 'utf-8');
        return;
    }

    response.writeHead(405, headers);
    response.end(`${request.method} is not allowed for the request.`);

}).listen(port); //imposto il server per ascoltare sulla porta indicata
console.log('Server running at http://127.0.0.1:' + port + '/');
Su GitHub sono presenti degli esempi completi in express e .Net 5

Client

Lato client dobbiamo modificare la fetch JavaScript per includere il parametro mode: "cors":

HTML: index.html

<html>
  <head>
    <title>Demo Client CORS - Sgart.it</title>
  </head>

  <body>
    <h1>Demo chiamate AJAX su un dominio differente</h1>

    <h3>Risultato chiamata</h3>
    <hr />
    <div id="result"></div>
    <hr />
    <button id="btn-reload" type="button">Reload</button>
    <script>
      function load() {
        const elem = document.getElementById("result");

        //fetch("http://cors.localhost:3000/", {  // non funziona per le restrizioni del browsers
        fetch("http://api.sgart.it:3000", {
          method: "POST",
          mode: "cors", // parametro vincolante per le chiamate cross domain
          headers: {
            "Content-Type": "application/json",
            //Authorization: "Bearer mH4eyJdb...r2E2JRHDcdfxjoYtg",
          },
          //body: JSON.stringify({ d: 1 }),
        })
          .then((response) => response.json())
          .then((data) => {
            console.log(data);
            elem.innerHTML = JSON.stringify(data);
          })
          .catch((error) => {
            console.error(error);
            elem.innerHTML = error;
          });
      }

      document.addEventListener("DOMContentLoaded", function () {
        document
          .getElementById("btn-reload")
          .addEventListener("click", function () {
            load();
          });
        load();
      });
    </script>
  </body>
</html>

Note

Le chiamate CORS sono bloccate sull'indirizzo localhost.

Per aggirare l'ostacolo e e creare un ambiente il più possibile simile a quello reale, si può modificare il file C:\Windows\System32\drivers\etc\hosts e includere i nomi dei 2 domini in modo che puntino a 127.0.0.1, ad esempio:

Text: C:\Windows\System32\drivers\etc\hosts

...
127.0.0.1	static.sgart.it
127.0.0.1	api.sgart.it

Autenticazione

Quello fin qui descritto è valido per applicazioni che non espongono dati riservati e che non richiedono un autenticazione.
Infatti chiunque potrebbe richiamare l'API, ad esempio da Powershell.

Questo solo per sottolineare che le intestazioni CORS non sono una forma di autenticazione e/o autorizzazioni delle API. Vengono interpretate solo dal browser per effettuare chiamate in sicurezza.

Prossimamente mostrerò un esempio che utilizza l'autenticazione tramite JSON Web Token per autenticare ed autorizzare il client ad accedere alle API.

Il progetto completo è disponibile su GitHub.
Tags:
Angular7 HTML74 HTML 554 JavaScript184 NodeJs21 React17 Vanilla JS24
Potrebbe interessarti anche: