Skip to content

18. [Cours]: Gestione degli accessi tra domini

Parole chiave: CORS (Cross-Origin Resource Sharing).

Questo capitolo è leggermente fuori tema rispetto al TD. È stato mantenuto perché introduce la programmazione web e la programmazione JavaScript. Va ricordato che uno degli obiettivi di questo TD è quello di presentare i concetti frequentemente utilizzati nello sviluppo JEE, ovvero lo sviluppo web basato su framework Java. Qui si completa il server web utilizzato nello studio del database dei prodotti e delle categorie per consentirgli di accettare richieste tra domini diversi.

Nel documento [Tutoriel AngularJS / Spring 4], si sviluppa un’applicazione client/server in cui il client è un’applicazione AngularJS:

  • le pagine HTML / CSS / JS dell’applicazione Angular provengono dal server [1];
  • in [2], il servizio [dao] effettua una richiesta a un altro server, il server [2]. Ebbene, ciò è vietato dal browser che esegue l’applicazione Angular perché costituisce una falla di sicurezza. L’applicazione può interrogare solo il server da cui proviene, ovvero il server [1];

In realtà, non è corretto affermare che il browser impedisca all’applicazione Angular di interrogare il server [2]. L’applicazione lo interroga infatti per chiedergli se autorizza un client esterno a interrogarlo. Questa tecnica di condivisione è denominata CORS (Cross-Origin Resource Sharing). Il server [2] dà il proprio consenso inviando specifici header HTTP.

Creeremo la seguente architettura:

  • in [1], un’applicazione web fornisce le pagine HTML / jS;
  • in [2], il browser esegue il JavaScript incorporato nelle pagine HTML per interrogare il servizio web protetto [3];

18.1. Support

  

I progetti di questo capitolo si trovano nella cartella [support / chap-18].

18.2. Il progetto del cliente

Si crea il seguente progetto Eclipse:

  

18.3. Configurazione Maven

Il progetto è un progetto Maven con il seguente file [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.webjson</groupId>
    <artifactId>intro-server-webjson-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>intro-server-webjson-01</name>
    <description>démo spring mvc</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>istia.st.springdata</groupId>
            <artifactId>intro-spring-data-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • righe 11-15: si tratta di un progetto Spring Boot;
  • righe 23-26: si utilizza la dipendenza [spring-boot-starter-web] che include un server Tomcat e Spring MVC;

18.4. Configurazione Spring

  

La classe [WebConfig] che configura il progetto Spring è la seguente:


package spring.cors.client.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    // -------------------------------- configurazione del livello [web]
    @Autowired
    private ApplicationContext context;

    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
        return servlet;
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }

    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8081);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/*.html").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("/*.js").addResourceLocations("classpath:/static/js/");
    }
}
  • riga 15: la classe configura un progetto Spring MVC;
  • riga 16: la classe estende la classe [WebMvcConfigurerAdapter] per ridefinire alcuni dei suoi metodi;
  • righe 18-36: abbiamo già incontrato questi bean, ad esempio nel paragrafo 13.5.3.1. Si noti, alla riga 35, che il servizio web funzionerà sulla porta 8081;
  • righe 38-42: il metodo [addResourceHandlers] consente di definire risorse statiche, ovvero risorse non gestite dal metodo [DispatcherServlet] della riga 23;
  • riga 40: ogni richiesta di una risorsa con estensione .html riceverà come risposta il file richiesto dalla richiesta e presente nella cartella [static] del Classpath del progetto;
  • riga 41: ogni richiesta di una risorsa con estensione .js riceverà come risposta il file JavaScript richiesto dalla richiesta e presente nella cartella [static/js] del Classpath del progetto;
  

18.5. Nozioni di base su jQuery e JavaScript

La pagina HTML del client sarà la seguente:

 

Conterrà del codice JavaScript (jS) eseguito nel browser. Presenteremo alcune nozioni di base su JavaScript che ci permetteranno di comprendere il codice. Il client effettuerà chiamate HTTP utilizzando la libreria jQuery [https://jquery.com/], che offre numerose funzioni che facilitano lo sviluppo in JavaScript. Creiamo un file statico HTML [jQuery.html] che inseriamo nella cartella [static]:

 

Questo file avrà il seguente contenuto:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>JQuery-01</title>
<script type="text/javascript" src="/jquery-2.1.3.min.js"></script>
</head>
<body>
    <h3>Rudiments de JQuery</h3>
    <div id="element1">Elément 1</div>
</body>
</html>
  • riga 6: importazione di jQuery;
  • righe 10-12: un elemento della pagina con id [element1]. Ora sperimenteremo con questo elemento.

Dobbiamo scaricare il file [jquery-2.1.3.min.js]. Troveremo l’ultima versione di jQuery su URL [http://jquery.com/download/]:

Image

Si inserirà il file scaricato nella cartella [static / js] e si modificherà la riga 6 del file HTML in base alla versione installata.

Fatto ciò, si richiederà la vista statica [jQuery.html] con Chrome [1-2]:

Con Google Chrome, digitare [Ctrl-Maj-I] per visualizzare gli strumenti di sviluppo [3]. La scheda [Console] [4] consente di eseguire codice JavaScript. Di seguito forniamo alcuni comandi JavaScript da digitare e ne forniamo una spiegazione.

JS
résultat
$("#element1")
: crea la collezione di tutti gli elementi con id [element1],
quindi normalmente una collezione di 0 o 1 elemento
poiché non è possibile avere due ID identici in una pagina HTML.
$("#element1").text("blabla")
: assegna il testo [blabla] a tutti gli elementi
della collezione. Ciò ha l’effetto di modificare il
contenuto visualizzato dalla pagina
$("#element1").hide()
nasconde gli elementi della collezione.
Il testo [blabla] non viene più visualizzato.
$("#element1")
: visualizza nuovamente la collezione. Questo ci
permette di vedere che l'elemento con id [element1] ha
l'attributo CSS style='display: none;' che fa
che l'elemento sia nascosto.
$("#element1").show()
: visualizza gli elementi della collezione. Il testo
[blabla] riappare. È l'attributo
CSS style='display: block;' che garantisce questa
visualizzazione.
$("#element1").attr('style','color: red')
: assegna un attributo a tutti gli elementi della
collezione. L'attributo in questo caso è [style] e il suo valore
[color: red]. Il testo [blabla] diventa rosso.
Tableau
Dictionnaire

Si noti che il codice URL del browser non è cambiato durante tutte queste operazioni. Non c'è stato alcun scambio con il server web. Tutto avviene all'interno del browser. Ora visualizziamo il codice sorgente della pagina:

Questo è il testo iniziale. Non riflette in alcun modo le modifiche apportate all’elemento nelle righe 10-12. È importante tenerlo presente quando si esegue il debug di JavaScript. Spesso, quindi, è inutile visualizzare il codice sorgente della pagina visualizzata.

18.6. Il codice JavaScript dell'applicazione

Torniamo alla pagina dell’applicazione client che interrogherà il servizio web / jSON:

  
 

Il codice HTML di questa pagina è il seguente:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/jquery-2.1.3.min.js"></script>
<script type="text/javascript" src="/client.js"></script>
</head>
<body>
    <h2>Client du service web / jSON</h2>
    <form id="formulaire">
        <!-- ID -->
        Identifiant :
        <!--  -->
        <input type="text" id="identifiant" name="identifiant" value="" />
        <!--  password -->
        <br /> <br /> Mot de passe :
        <!--  -->
        <input type="text" id="password" name="password" value="" />
        <!--  metodo HTTP -->
        <br /> <br /> Méthode HTTP :
        <!--  -->
        <input type="radio" id="get" name="method" value="get"
            checked="checked" />GET
        <!--  -->
        <input type="radio" id="post" name="method" value="post" />POST
        <!--  URL -->
        <br /> <br />URL cible (commençant par /): <input type="text"
            id="url" size="30"><br />
        <!-- valore inserito -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted"
            size="50" />
        <!-- pulsante di conferma -->
        <br /> <br /> <input type="button" value="Valider"
            onclick="javascript:requestServer()"></input>
    </form>
    <hr />
    <h2>Réponse du serveur</h2>
    <div id="response"></div>
</body>
</html>
  • riga 6: si importa la libreria jQuery;
  • riga 7: si importa un codice che scriveremo noi;
  • righe 15, 19, 26, 29, 31: si annotano gli identificatori [id] dei componenti della pagina. Il codice JavaScript fa riferimento a questi componenti tramite tali identificatori;

Il codice [client.js] è il seguente:


// dati globali
var url;
var posted;
var response;
var method;
var baseUrl = 'http://localhost:8080';
var identifiant;
var password;
var authorizationHeader;

function requestServer() {
    // si recuperano le informazioni
    var urlValue = url.val();
    var postedValue = posted.val();
    var identifiantValue = identifiant.val();
    var passwordValue = password.val();
    var method = document.forms[0].elements['method'].value;
    authorizationCode = btoa(identifiantValue + ':' + passwordValue);
    // si cancella la risposta precedente
    response.text("");
    // si effettua una chiamata Ajax manualmente
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // si effettua una chiamata Ajax manualmente
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // risultato testo
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // errore di sistema
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}

function doPost(url, posted) {
    // si effettua una chiamata Ajax manualmente
    $.ajax({
        headers : {
            'Autorizzazione':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'POST',
        contentType : 'application/json; charset=UTF-8',
        data : posted,
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // risultato testo
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // errore di sistema
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}

// durante il caricamento del documento
$(document).ready(function() {
    // si recuperano i riferimenti dei componenti della pagina
    identifiant = $("#identifiant");
    password = $("#password");
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • righe 80-87: codice jS eseguito al termine del caricamento del documento nel browser;
  • righe 81-86: si recuperano i riferimenti dei diversi elementi del documento HTML, tramite il loro identificatore [id];
  • righe 2-9: variabili globali note in tutte le funzioni definite nel file jS;
  • riga 13: si recupera il valore URL digitato dall'utente;
  • riga 14: si recupera il valore che l'utente desidera inviare (vuoto se l'operazione è GET);
  • riga 15: si recupera l'ID inserito dall'utente;
  • riga 16: si recupera la sua password;
  • riga 17: si recupera il metodo [get] o [post] da utilizzare per richiedere l'URL della riga 9:
    • [document] indica il documento caricato dal browser, ovvero il cosiddetto DOM (Document Object Model),
    • [document.forms[0]] indica il primo modulo del documento; un documento può contenerne diversi. In questo caso ce n’è solo uno,
    • [document.forms[0].elements['method']] indica l'elemento del modulo che possiede l'attributo [name='method']. Ce ne sono due:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • (continua)
    • [document.forms[0].elements['method'].value] è il valore che verrà inviato per il componente che possiede l'attributo [name='method']. Si sa che il valore inviato è il valore dell’attributo [value] del pulsante di opzione selezionato. In questo caso, sarà quindi una delle stringhe ['get', 'post'];
  • riga 18: si costruisce la codifica Base74 della stringa identificativo:password. Questa stringa codificata verrà utilizzata nell’intestazione HTTP [Authorization] che invieremo al server per autenticare la richiesta;
  • righe 22-26: a seconda del metodo HTTP da utilizzare, si esegue il metodo [doGet] o [doPost];
  • il metodo jQuery [$.ajax] effettua una chiamata HTTP;
  • righe 32-34: ci si connette a un server che richiede un'intestazione HTTP o [Authorization: Basic code];
  • riga 35: l'utente inserirà dei URL del tipo [/cors-getAllCategories,/cors-addProduits, ...]. È quindi necessario completare questi URL con il URL del server della riga 6;
  • riga 36: metodo HTTP da utilizzare;
  • riga 37: il server restituisce il codice jSON. Si indica il tipo [text] come tipo di risultato per visualizzarlo così come è stato ricevuto;
  • riga 42: visualizzazione della risposta testuale del server;
  • righe 48-49: visualizzazione dell'eventuale messaggio di errore;
  • riga 53: il metodo [doPost] riceve un secondo parametro che è il valore da inviare;
  • riga 61: per indicare che il valore inviato sarà sotto forma di stringa jSON;

18.7. Esecuzione del client

L'applicazione client è un'applicazione Spring Boot avviata dalla seguente classe eseguibile [Boot]:

  

package spring.cors.client.boot;

import org.springframework.boot.SpringApplication;

import spring.cors.client.config.WebConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(WebConfig.class, args);
    }
}
  • riga 10: il metodo [SpringApplication.run] utilizza il file di configurazione [WebConfig]. La pagina [client.html] verrà distribuita sul server Tomcat presente nel Classpath del progetto;

18.8. L'URL [/getAllCategories]

Stiamo avviando:

  • il server web/json sulla porta 8080;
  • il client di questo server sulla porta 8081;

quindi richiediamo il URL [http://localhost:8081/client.html] [1]:

  • in [2], eseguiamo un GET su URL [http://localhost:8080/getAllCategories];

Non riceviamo alcuna risposta dal server. Osservando la console di sviluppo di Chrome (Ctrl-Maiusc-I) si rileva un errore:

  • in [1], ci troviamo nella scheda [Network];
  • in [2], si nota che la richiesta HTTP effettuata non è [GET] ma [OPTIONS]. Nel caso di una richiesta interdominio, il browser verifica con il server che siano soddisfatte una serie di condizioni inviandogli una richiesta HTTP [OPTIONS]. In questo caso, le richieste sono quelle indicate dai simboli [5-6];
  • in [5], il browser chiede se la destinazione URL sia raggiungibile tramite un GET. L’intestazione della richiesta [Access-Control-Request-Method] richiede una risposta con un’intestazione HTTP [Access-Control-Allow-Methods] che indichi che il metodo richiesto è accettato;
  • in [6], il browser invia l'intestazione HTTP [Origin: http://localhost:8081]. Questa intestazione richiede una risposta in un'intestazione HTTP [Access-Control-Allow-Origin] che indichi che l'origine specificata è accettata;
  • in [7], il browser chiede se le intestazioni HTTP, [accept] e [authorization] sono accettate. L'intestazione della richiesta [Access-Control-Request-Headers] attende una risposta con un'intestazione HTTP [Access-Control-Allow-Headers] che indichi che le intestazioni richieste sono accettate;
  • si verifica un errore in [3]. Cliccando sull’icona, si ottiene l’errore [4];
  • in [4], il messaggio indica che il server non ha inviato l’intestazione HTTP [Access-Control-Allow-Origin] che specifica se l’origine della richiesta è accettata;
  • in [8], si può constatare che il server non ha effettivamente inviato tale intestazione. Di conseguenza, il browser ha rifiutato di eseguire la richiesta HTTP GET inizialmente richiesta;

Dobbiamo modificare il server web / jSON.

18.9. Il nuovo servizio web / json

Creiamo un nuovo progetto Maven [intro-spring-cors-server-jpa]:

18.9.1. Configurazione Maven

La configurazione Maven del nuovo servizio web è la seguente:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.cors</groupId>
    <artifactId>spring-cors-server-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-cors-server-jpa</name>
    <description>démo spring cors</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>istia.st.spring.security</groupId>
            <artifactId>intro-spring-security-server-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <!-- plugin -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • righe 23-27: recuperiamo tutto il lavoro svolto finora attingendo all'archivio del server web / json protetto;

18.9.2. Configurazione Spring

La classe di configurazione [AppConfig] è la seguente:

  

package spring.cors.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import spring.security.config.SecurityConfig;

@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {

    // richieste interdominio
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
}
  • riga 10: la classe è una classe di configurazione Spring;
  • riga 11: altri componenti Spring sono da ricercare nel pacchetto [spring.cors.server.service];
  • righe 16-19: creiamo un componente Spring denominato [isCorsEnabled] che indica se accettare o meno i client esterni al dominio del server;

18.9.3. La classe [AbstractCorsController]

La classe [AbstractCorsController], che sarà la classe padre di tutti i controller di questa applicazione:

 

Il suo codice è il seguente:


package spring.cors.server.service;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;

public abstract class AbstractCorsController {

    @Autowired
    private boolean isCorsEnabled;

    // invio delle opzioni al client
    public void setHeaders(String origin, HttpServletResponse response) {
        // CORS consentito?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // si imposta l'header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // si autorizzano determinati header
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // si autorizza il GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
}
  • riga 7: la classe [CorsController] è astratta poiché è progettata per essere estesa e non istanziata;
  • righe 13-24: il metodo [setHeaders] inserisce nella risposta [HttpServletResponse response] (riga 13) inviata al client le intestazioni HTTP richieste dalle richieste interdominio;
  • riga 33: il metodo [/setHeaders] accetta come parametri:
    • la stringa [origin] presente nell'intestazione HTTP [Origin] delle richieste interdominio:
Origin:http://localhost:8081

In questo caso, il parametro [origin] della riga 13 avrebbe il valore [http://localhost:8081]. Nel caso in cui la richiesta non contenga l'intestazione HTTP [Origin], provvederemo a ottenere [origin==null];

  • (continua)
    • l'oggetto [HttpServletResponse response] che verrà restituito al cliente che ha effettuato la richiesta;

Questi due parametri vengono inseriti da Spring;

  • righe 15-175: se l'applicazione è configurata per accettare le richieste interdominio e se il mittente ha inviato l'intestazione HTTP [Origin] e se tale origine inizia con [http://localhost], allora si accetta la richiesta interdominio, altrimenti la si rifiuta;
  • riga 19: se il client si trova nel dominio [http://localhost:port], si invia l'intestazione HTTP:
Access-Control-Allow-Origin:  http://localhost:porta

il che significa che il server accetta l’origine del client;

  • riga 21: abbiamo segnalato due intestazioni HTTP specifiche nella richiesta HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

All'intestazione HTTP [Access-Control-Request-X], il server risponde con un'intestazione HTTP [Access-Control-Allow-X] in cui indica ciò che è consentito. Le righe 20-23 si limitano a riprendere la richiesta del client per indicare che è stata accettata;

18.9.4. Il controller [MyControllerWithHttpOptions]

Per evitare di dover modificare il server web non sicuro / jSON [intro-server-webjson-01] esaminato nel paragrafo 13.5.3, creeremo un nuovo controller che, laddove il server non sicuro elabora l’URL [/url], il nuovo controller gestirà i URL e [/cors-url], mentre il URL accetterà le richieste interdominio.

La classe [MyControllerWithHttpOptions] è il controller che gestirà le richieste HTTP di tipo [OPTIONS]:

 

package spring.cors.server.service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.fasterxml.jackson.core.JsonProcessingException;

@Controller
public class MyControllerWithHttpOptions extends AbstractCorsController {

    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.OPTIONS)
    public void getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse){
        // intestazioni CORS
        setHeaders(origin, httpServletResponse);
    }
...
  • riga 14: la classe è un controller Spring MVC;
  • riga 15: la classe [MyControllerWithHttpOptions] estende la classe [AbstractCorsController] che abbiamo appena descritto;
  • righe 17-18: il metodo [getAllCategories] (riga 18) elabora URL ["/cors-getAllCategories"] quando viene richiamata con il metodo HTTP [OPTIONS];
  • riga 18: il metodo [getAllCategories] accetta due parametri:
    • [@RequestHeader(value = "Origin", required = false) String origin] per recuperare il valore dell'intestazione HTTP [Origin:http://localhost:8081] quando presente. In questo esempio, il parametro [String origin] riceverà il valore [http://localhost:8081]. Questa intestazione non è obbligatoria [required = false]. Quando non è presente, il parametro [String origin] assumerà il valore null;
    • [HttpServletResponse httpServletResponse]: la risposta che verrà inviata al client;
  • riga 21: si inviano le intestazioni HTTP che consentono le richieste interdominio. Il metodo [setHeaders] è definito nella classe padre [AbstractCorsController];

Si procede in questo modo per tutti i URL esposti dal server web / jSON non protetto [intro-server-webjson-01] esaminato nel paragrafo 13.5.3. Quando questo servizio espone URL e [/url], la classe [MyControllerWithHttpOptions] sopra indicata espone URL e [/cors-url].

18.9.5. Il controller [MyControllerWithCors]

 

La classe [MyControllerWithCors] è il controller che elaborerà le richieste HTTP di tipo [GET] e [POST]:


package spring.cors.server.service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;

import spring.webjson.service.MyController;

@Controller
public class MyControllerWithCors extends AbstractCorsController {

    // dipendenze Spring
    @Autowired
    private MyController myController;

...
    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // risposta
        return myController.getAllCategories();
    }
...
  • riga 17: la classe [MyControllerWithCors] è un controller Spring MVC
  • riga 18: estende la classe [AbstractCorsController];
  • righe 21-22: iniezione del controller [MyController] dal server web / jSON non protetto [intro-server-webjson-01] analizzato nel paragrafo 13.5.3;
  • righe 25-27: il metodo [getAllCategories] elabora URL [/cors-getAllCategories] (riga 28) quando richiesto con il metodo HTTP [GET];
  • riga 26: il risultato del metodo [getAllCategories] verrà inviato al cliente. Tale risultato è un flusso jSON (attributo [produces] della riga 27 e tipo [String] del risultato della riga 25);
  • riga 27: il metodo riceve gli stessi parametri del metodo [getAllCategories] del controller [MyControllerWithHttpOptions] che abbiamo appena esaminato;
  • riga 30: si richiede al metodo [myController.getAllCategories()] di inviare la risposta;

Alla fine, è il metodo [myController.getAllCategories()] del server non sicuro che invia la risposta. Abbiamo semplicemente arricchito la sua risposta con le intestazioni necessarie per le richieste interdominio.

Si procede in questo modo per tutti i URL esposti dal server web / jSON non sicuro [intro-server-webjson-01] esaminato nel paragrafo 13.5.3. Quando questo servizio espone l’URL e l’[/url], la classe [MyControllerWithCors] sopra indicata espone l’URL e l’[/cors-url].

Una richiesta interdominio si svolgerà nel modo seguente:

  • il codice JS del cliente richiede ilURL e [/cors-url] con una richiesta HTTP, GET o POST;
  • il browser che esegue questo codice intercetta tale richiesta e richiede innanzitutto ilURL [/cors-url] con una richiesta HTTP OPTIONS per verificare che il servizio web di destinazione accetti le richieste interdominio;
  • uno dei metodi del controller [MyControllerWithHttpOptions] invia le intestazioni interdominio attese dal browser;
  • il browser invia quindi la richiesta iniziale URL ([/cors-url]) con una richiesta HTTP, GET o POST;
  • uno dei metodi del controller [MyControllerWithCors] risponde quindi alla richiesta;

18.9.6. Test

La classe di avvio del progetto [intro-spring-cors-server-jpa] è la seguente:

  

package spring.cors.server.boot;

import org.springframework.boot.SpringApplication;

import spring.cors.server.config.AppConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}
  • riga 10: il metodo statico [SpringApplication.run] viene eseguito con la configurazione Spring [AppConfig]. Grazie a questa configurazione, viene avviato il server Tomcat integrato nell'archivio del progetto e su di esso viene distribuita l'applicazione web [intro-spring-cors-server-jpa]. Su di esso viene distribuita anche l’applicazione web del server non protetto [intro-server-webjson-01], che fa parte degli archivi del progetto. Poiché anche il progetto [intro-spring-security-server-01] fa parte degli archivi, vengono infine esposti due tipi di URL:
    • quelle del servizio web protetto: /url;
    • quelle del servizio web che accetta richieste interdominio: /cors-url;

Ora siamo pronti per nuovi test. Lanciamo la nuova versione del servizio web e scopriamo che il problema persiste. Non è cambiato nulla. Se alla riga 7 qui sotto inseriamo un output di console, questo non viene mai visualizzato, dimostrando così che il metodo [getAllCategories] della classe [MyControllerWithHttpOptions] non viene mai chiamato;


@Controller
public class MyControllerWithHttpOptions extends AbstractCorsController {

    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.OPTIONS)
    public void getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse){
        System.out.println(un_texte) ;
        // intestazioni CORS
        setHeaders(origin, httpServletResponse);
    }

Dopo alcune ricerche, si scopre che, per impostazione predefinita, Spring MVC gestisce autonomamente i comandi HTTP e [OPTIONS]. Pertanto è sempre Spring a rispondere e mai il metodo [getAllCategories] della riga 5 sopra riportata. Questo comportamento predefinito di Spring MVC può essere modificato. Modifichiamo la classe [AppConfig] esistente:

  

package spring.cors.server.config;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.DispatcherServlet;

import spring.security.config.SecurityConfig;

@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {

    // richieste interdominio
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @PostConstruct
    public void init() {
        // l'applicazione gestisce autonomamente le richieste HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}
  • righe 25-26: iniezione del bean [dispatcherServlet] che gestisce le richieste dei client. Questo bean è stato definito nella configurazione del server web / jSON non protetto [intro-server-webjson-01] esaminato nel paragrafo 13.5.3;
  • righe 28-29: il metodo [init] (riga 29) verrà eseguito non appena la classe [AppConfig] sarà stata istanziata e saranno state effettuate le iniezioni Spring. Pertanto, al momento della sua esecuzione, il campo della riga 26 sarà già stato inizializzato;
  • riga 31: si configura il bean [dispatcherServlet] in modo che lasci all’applicazione web il compito di gestire autonomamente i comandi HTTP e [OPTIONS];

Ripetiamo i test con questa nuova configurazione. Si ottiene il seguente risultato:

  • in [1], notiamo che ci sono due richieste HTTP verso URL e [http://localhost:8080/cors-getAllCategories];
  • in [2], la richiesta [OPTIONS];
  • in [3], le tre intestazioni HTTP che abbiamo appena configurato nella risposta del server;

Esaminiamo ora la seconda richiesta:

  • in [1], la richiesta esaminata;
  • in [2], ovvero la richiesta GET. Grazie alla prima richiesta [OPTIONS], il browser ha ricevuto le informazioni richieste. Ora esegue la richiesta [GET] richiesta inizialmente;
  • in [3], la risposta del server;
  • in [4], il server invia jSON;
  • in [5], si è verificato un errore;
  • in [6], il messaggio di errore;

È più difficile spiegare cosa sia successo in questo caso. La risposta [3] del server è normale, ovvero [HTTP/1.1 200 OK]. Dovremmo quindi avere il documento richiesto. È possibile che il server abbia effettivamente inviato il documento, ma che sia il browser a impedirne l’utilizzo perché richiede che anche per la richiesta GET la risposta contenga l’intestazione HTTP [Access-Control-Allow-Origin:http://localhost:8081].

Modifichiamo quindi il controller [MyControllerWithCors] in modo che anch’esso invii le intestazioni necessarie alle richieste interdominio:


    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // intestazioni CORS
        setHeaders(origin, httpServletResponse);
        // risposta
        return myController.getAllCategories();
}
  • riga 6: le intestazioni necessarie per le richieste interdominio sono incluse nella risposta;

Dopo questa modifica, i risultati sono i seguenti:

Abbiamo ottenuto correttamente l’elenco delle categorie.

18.10. Gli altri URL [GET]

Nei controller [MyControllerWithCors, MyControllerWithHttpOptions], il codice delle azioni che gestiscono le richieste URL con un [GET] segue il modello delle azioni che in precedenza hanno gestito URL e [/cors-getAllCategories]. Il lettore può verificare il codice negli esempi forniti con il presente documento. Ecco un esempio per URL e [/cors-getAllProduits]:

in [MyControllerWithHttpOptions]


    @RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.OPTIONS)
    public void getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) {
        // intestazioni CORS
        setHeaders(origin, httpServletResponse);
}

in [MyControllerWithCors]


    @RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // intestazioni CORS
        setHeaders(origin, httpServletResponse);
        // risposta
        return myController.getAllProduits();
}

Il risultato ottenuto è il seguente:

18.11. I URL [POST]

Esaminiamo il seguente caso:

  • si effettua un POST [1] verso il URL [2];
  • in [3], il valore inserito. Si tratta di una stringa jSON;
  • in totale, si cerca di creare una categoria denominata [categorie2];

Per il momento non modifichiamo alcun codice. Il risultato ottenuto è il seguente:

  • in [1], come per le richieste [GET], il browser effettua una richiesta [OPTIONS];
  • in [2], richiede un'autorizzazione di accesso per una richiesta [POST]. In precedenza era [GET];
  • in [3], richiede l’autorizzazione a inviare le intestazioni HTTP e [accept, authorization, content-type]. In precedenza erano presenti solo le prime due intestazioni;
  • in [4], il servizio web non concede tutte le autorizzazioni richieste, causando l'errore [5];

Modifichiamo il metodo [AbstractController.sendHeaders] nel modo seguente:


package spring.cors.server.service;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;

public abstract class AbstractCorsController {

    @Autowired
    private boolean isCorsEnabled;

    // invio delle opzioni al cliente
    public void setHeaders(String origin, HttpServletResponse response) {
        // Cors consentito?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // si imposta l'intestazione CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // si autorizzano alcune intestazioni
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // si autorizzano GET e POST
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
    }
}
  • riga 21: è stata aggiunta l'intestazione HTTP [Content-Type] (le maiuscole e le minuscole non fanno differenza);
  • riga 23: è stato aggiunto il metodo HTTP [POST];

In questo modo i metodi [POST] vengono trattati allo stesso modo delle richieste [GET]. Ecco l'esempio di URL [/cors-addArticles]:

in [MyControllerWithCors]


    @RequestMapping(value = "/cors-addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse httpServletResponse)
                    throws JsonProcessingException {
        // intestazioni CORS
        setHeaders(origin, httpServletResponse);
        // risposta
        return myController.addCategories(request);
}

in [MyControllerWithHttpOptions]


    @RequestMapping(value = "/cors-addCategories", method = RequestMethod.OPTIONS)
    public void addCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse httpServletResponse)
                    throws JsonProcessingException {
        // intestazioni CORS
        setHeaders(origin, httpServletResponse);
}

Il risultato ottenuto è il seguente:

 

La categoria [categorie2] è stata correttamente aggiunta al database. Il SGBD le ha assegnato la chiave primaria 1729.

18.12. Il controller [AuthenticateCorsController]

  

Il controller [AuthenticateCorsController] serve a fornire ilURL [/cors-authenticate] che consente di richiamare il URL [/authenticate] già esistente, con una richiesta interdominio. Il suo codice è il seguente:


package spring.cors.server.service;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;

import spring.security.service.AuthenticateController;

@Controller
public class AuthenticateCorsController extends AbstractCorsController {
    @Autowired
    private AuthenticateController authenticateController;

    @RequestMapping(value = "/cors-authenticate", method = RequestMethod.GET)
    @ResponseBody
    public String authenticate(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) throws JsonProcessingException {
        // intestazioni CORS
        setHeaders(origin, response);
        // metodo di origine
        return authenticateController.authenticate();
    }

    @RequestMapping(value = "/cors-authenticate", method = RequestMethod.OPTIONS)
    public void corsAuthenticate(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        // intestazioni CORS
        setHeaders(origin, response);
    }

}

Ecco due esempi:

  • le risposte visualizzate sono generate dal codice jS:

function doGet(url) {
    // si effettua manualmente una chiamata Ajax
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // risultato testuale
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // errore di sistema
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
  • la risposta [1] viene visualizzata dalla riga 14 della funzione [success];
  • la risposta [2] viene visualizzata dalla riga 20 della funzione [error]. La funzione [JSON.stringify] crea la stringa jSON dell'oggetto [jqXHR.statusCode()], che è l'oggetto che incapsula l'errore verificatosi. Questo oggetto fornisce poche informazioni. È possibile utilizzare altri metodi dell’oggetto [jqXHR] per ottenere, ad esempio, le intestazioni HTTP restituite dal server;

18.13. Conclusion

La nostra applicazione ora supporta le richieste interdominio. Queste possono essere autorizzate o meno tramite configurazione nella classe [AppConfig]:


@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {

    // richieste interdominio
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @PostConstruct
    public void init() {
        // l'applicazione gestisce autonomamente le richieste HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}