Skip to content

18. [Corso]: Condivisione delle risorse tra origini diverse

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

Questo capitolo esula in parte dall'ambito del tutorial. È stato incluso perché introduce la programmazione web e la programmazione JavaScript. È importante ricordare che uno degli obiettivi di questo tutorial è presentare concetti frequentemente utilizzati nello sviluppo JEE, ovvero lo sviluppo web basato su framework Java. Qui estendiamo il server web utilizzato nello studio del database di prodotti e categorie per consentirgli di accettare richieste cross-domain.

Nel documento [Tutorial AngularJS / Spring 4], sviluppiamo 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]. Tuttavia, ciò è vietato dal browser che esegue l'applicazione Angular poiché costituisce una vulnerabilità di sicurezza. L'applicazione può interrogare solo il server da cui ha origine, ovvero il server [1];

In realtà, non è corretto dire che il browser impedisca all'applicazione Angular di interrogare il server [2]. In realtà lo interroga per chiedere se consente a un client che non proviene da esso di interrogarlo. Questa tecnica di condivisione è chiamata CORS (Cross-Origin Resource Sharing). Il server [2] concede l'autorizzazione inviando specifici header HTTP.

Creeremo la seguente architettura:

  • in [1], un'applicazione web fornisce pagine HTML/JS;
  • in [2], il browser esegue il JavaScript incorporato nelle pagine HTML per interrogare il servizio web sicuro [3];

18.1. Supporto

  

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

18.2. Il progetto client

Creare 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: questo è un progetto Spring Boot;
  • righe 23–26: utilizziamo la dipendenza [spring-boot-starter-web], che include un server Tomcat e Spring MVC;

18.4. Configurazione di 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 {
 
    // -------------------------------- layer configuration [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 sovrascrivere alcuni dei suoi metodi;
  • righe 18–36: abbiamo già incontrato questi bean, ad esempio nella sezione 13.5.3.1. Si noti, alla riga 35, che il servizio web verrà eseguito sulla porta 8081;
  • righe 38–42: il metodo [addResourceHandlers] consente di definire risorse statiche, ovvero risorse non gestite dal [DispatcherServlet] alla riga 23;
  • riga 40: qualsiasi richiesta di una risorsa con estensione .html restituirà il file richiesto dalla richiesta e presente nella cartella [static] del classpath del progetto;
  • riga 41: qualsiasi richiesta di una risorsa con estensione .js restituirà 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 cliente sarà la seguente:

 

Includerà codice JavaScript (JS) che verrà eseguito nel browser. Tratteremo alcune nozioni di base su JavaScript per aiutarci a comprendere il codice. Il client effettuerà richieste HTTP utilizzando la libreria jQuery [https://jquery.com/], che fornisce molte funzioni che semplificano lo sviluppo in JavaScript. Creiamo un file HTML statico [jQuery.html] e lo 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 l'ID [element1]. Lavoreremo con questo elemento.

Dobbiamo scaricare il file [jquery-2.1.3.min.js]. L'ultima versione di jQuery è disponibile all'URL [http://jquery.com/download/]:

Image

Inserisci il file scaricato nella cartella [static/js] e aggiorna la riga 6 del file HTML in modo che corrisponda alla versione installata.

Una volta fatto ciò, apri la vista statica [jQuery.html] in Chrome [1-2]:

In Google Chrome, premi [Ctrl-Shift-I] per aprire gli strumenti di sviluppo [3]. La scheda [Console] [4] ti permette di eseguire codice JavaScript. Di seguito, ti forniamo i comandi JavaScript da digitare e ti spieghiamo a cosa servono.

JS
risultato
$("#element1")
: restituisce l'insieme di tutti gli elementi con l'ID [element1],
quindi normalmente una raccolta di 0 o 1 elemento
, poiché non è possibile avere due ID identici in una pagina HTML.
$("#element1").text("blabla")
: imposta il testo [blabla] per tutti gli elementi
nella raccolta. Questo modifica il
contenuto visualizzato dalla pagina
$("#element1").hide()
nasconde gli elementi nella collezione.
Il testo [blabla] non viene più visualizzato.
$("#element1")
: visualizza nuovamente la raccolta. Questo
ci permette di vedere che l'elemento con l'ID [element1] ha
l'attributo CSS style='display: none;', che
fa sì che l'elemento sia nascosto.
$("#element1").show()
: visualizza gli elementi della collezione. Il testo
[blabla] ricompare. È l'
style='display: block;' che garantisce questa
visualizzazione.
$("#element1").attr('style','color: red')
: imposta un attributo su tutti gli elementi della
collezione. L'attributo in questo caso è [style] e il suo valore
[color: red]. Il testo [blabla] diventa rosso.
Array
Dizionario

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

Questo è il testo iniziale. Non riflette le modifiche che abbiamo apportato all'elemento nelle righe 10-12. È importante tenerlo presente durante il debug di JavaScript. Spesso, quindi, non è necessario 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">
        <!--  identifier -->
        Identifiant :
        <!--  -->
        <input type="text" id="identifiant" name="identifiant" value="" />
        <!--  password -->
        <br /> <br /> Mot de passe :
        <!--  -->
        <input type="text" id="password" name="password" value="" />
        <!--  method 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 />
        <!-- posted value -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted"
            size="50" />
        <!-- validation button -->
        <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: importiamo la libreria jQuery;
  • riga 7: importiamo il codice che scriveremo;
  • Righe 15, 19, 26, 29, 31: Notate gli identificatori [id] dei componenti della pagina. Il JavaScript fa riferimento a questi componenti tramite tali identificatori;

Il codice [client.js] è il seguente:


// global data
var url;
var posted;
var response;
var method;
var baseUrl = 'http://localhost:8080';
var identifiant;
var password;
var authorizationHeader;
 
function requestServer() {
    // information retrieval
    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);
    // delete the previous answer
    response.text("");
    // make a manual Ajax call
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}
 
function doGet(url) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
 
function doPost(url, posted) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'POST',
        contentType : 'application/json; charset=UTF-8',
        data : posted,
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve page component references
    identifiant = $("#identifiant");
    password = $("#password");
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • righe 80–87: codice JavaScript eseguito dopo che il documento ha terminato il caricamento nel browser;
  • righe 81-86: recupera i riferimenti dei vari elementi nel documento HTML tramite i loro identificatori [id];
  • righe 2-9: variabili globali accessibili in tutte le funzioni definite nel file JavaScript;
  • riga 13: recupera l'URL inserito dall'utente;
  • riga 14: recupera il valore che l'utente desidera inviare (vuoto se si tratta di un'operazione GET);
  • riga 15: recupera il nome utente inserito dall'utente;
  • riga 16: recupera la password;
  • riga 17: recupera il metodo [get] o [post] da utilizzare quando si richiede l'URL della riga 9:
    • [document] si riferisce al documento caricato dal browser, noto come DOM (Document Object Model),
    • [document.forms[0]] si riferisce al primo modulo nel documento; un documento può contenere più moduli. Qui ce n'è solo uno,
    • [document.forms[0].elements['method']] si riferisce all'elemento del modulo con 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 con l'attributo [name='method']. Sappiamo che il valore inviato è il valore dell'attributo [value] del pulsante di opzione selezionato. Qui, quindi, sarà una delle stringhe ['get', 'post'];
  • riga 18: costruiamo la codifica Base74 della stringa `username: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, eseguiamo il metodo [doGet] o [doPost];
  • Il metodo jQuery [$.ajax] effettua una richiesta HTTP;
  • righe 32–34: comunichiamo con un server che richiede un'intestazione HTTP [Authorization: Basic code];
  • riga 35: l'utente inserirà URL del tipo [/cors-getAllCategories,/cors-addProduits, ...]. Questi URL devono quindi essere integrati con l'URL del server della riga 6;
  • riga 36: metodo HTTP da utilizzare;
  • riga 37: il server restituisce JSON. Specifichiamo il tipo [text] come tipo di risultato per visualizzarlo esattamente come ricevuto;
  • riga 42: visualizza la risposta di testo del server;
  • righe 48-49: visualizza eventuali messaggi 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 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]

Avviamo:

  • il server web/JSON sulla porta 8080;
  • il client per questo server sulla porta 8081;

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

  • in [2], eseguiamo una richiesta GET sull'URL [http://localhost:8080/getAllCategories];

Non riceviamo alcuna risposta dal server. Quando controlliamo la console degli sviluppatori di Chrome (Ctrl-Shift-I), vediamo un errore:

  • in [1], ci troviamo nella scheda [Rete];
  • In [2], vediamo che la richiesta HTTP effettuata non è [GET] ma [OPTIONS]. Nel caso di una richiesta cross-domain, il browser verifica con il server che determinate condizioni siano soddisfatte inviando una richiesta HTTP [OPTIONS]. In questo caso, le richieste sono quelle indicate dagli indicatori [5-6];
  • in [5], il browser chiede se l'URL di destinazione sia raggiungibile con 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 di richiesta [Access-Control-Request-Headers] si aspetta una risposta con un'intestazione HTTP [Access-Control-Allow-Headers] che indichi che le intestazioni richieste sono accettate;
  • si verifica un errore in [3]. Facendo clic 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], possiamo vedere che il server effettivamente non ha inviato questa intestazione. Di conseguenza, il browser ha rifiutato di effettuare 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 per il 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>
 
    <!-- plugins -->
    <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 tutti i dati del lavoro svolto finora accedendo all'archivio /json del server web sicuro;

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 {
 
    // cross-domain queries
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
}
  • riga 10: la classe è una classe di configurazione Spring;
  • riga 11: altri componenti Spring si trovano nel pacchetto [spring.cors.server.service];
  • righe 16–19: creiamo un componente Spring denominato [isCorsEnabled] che indica se i client al di fuori del dominio del server sono accettati o meno;

18.9.3. La classe [AbstractCorsController]

La classe [AbstractCorsController], che sarà la classe padre di tutti i controller in 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;
 
    // sending options to the customer
    public void setHeaders(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // we authorize GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
}
  • riga 7: la classe [CorsController] è astratta perché è progettata per essere estesa, non istanziata;
  • righe 13–24: il metodo [setHeaders] aggiunge le intestazioni HTTP richieste dalle richieste cross-domain alla [HttpServletResponse response] (riga 13) inviata al client;
  • riga 33: il metodo [setHeaders] accetta come parametri:
    • la stringa [origin] presente nell'intestazione HTTP [Origin] delle richieste cross-domain:
Origin:http://localhost:8081

In questo caso, il parametro [origin] alla riga 13 avrebbe il valore [http://localhost:8081]. Se la richiesta non contiene l'intestazione HTTP [Origin], ci assicureremo che [origin==null];

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

Questi due parametri vengono iniettati da Spring;

  • righe 15–175: se l'applicazione è configurata per accettare richieste cross-domain, e se il mittente ha inviato l'intestazione HTTP [Origin], e se questa origine inizia con [http://localhost], allora la richiesta cross-domain viene accettata; altrimenti, viene rifiutata;
  • Riga 19: se il client si trova nel dominio [http://localhost:port], inviamo l'intestazione HTTP:

Access-Control-Allow-Origin:  http://localhost:port

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

  • riga 21: abbiamo specificato due header HTTP specifici nella richiesta HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

In risposta all'intestazione HTTP [Access-Control-Request-X], il server risponde con un'intestazione HTTP [Access-Control-Allow-X] che specifica ciò che è consentito. Le righe 20–23 ripetono semplicemente la richiesta del client per indicare che è stata accettata;

18.9.4. Il controller [MyControllerWithHttpOptions]

Per evitare di dover modificare il server web/JSON non sicuro [intro-server-webjson-01] discusso nella Sezione 13.5.3, creeremo un nuovo controller. Mentre il server non sicuro gestisce l'URL [/url], il nuovo controller gestirà l'URL [/cors-url], e questo URL accetterà le richieste cross-origin.

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){
        // headers CORS
        setHeaders(origin, httpServletResponse);
    }
...
  • riga 14: la classe è un controller Spring MVC;
  • riga 15: la classe [MyControllerWithHttpOptions] estende la classe [AbstractCorsController] appena descritta;
  • righe 17–18: il metodo [getAllCategories] (riga 18) gestisce l'URL ["/cors-getAllCategories"] quando richiesto 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] avrà il valore null;
    • [HttpServletResponse httpServletResponse]: la risposta che verrà inviata al client;
  • riga 21: inviamo le intestazioni HTTP che abilitano le richieste cross-origin. Il metodo [setHeaders] è definito nella classe padre [AbstractCorsController];

Questo viene fatto per tutti gli URL esposti dal server web/JSON non protetto [intro-server-webjson-01] discusso nella Sezione 13.5.3. Quando questo servizio espone l'URL [/url], la classe [MyControllerWithHttpOptions] sopra espone l'URL [/cors-url].

18.9.5. Il controller [MyControllerWithCors]

 

La classe [MyControllerWithCors] è il controller che gestirà le richieste HTTP [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 {
 
    // spring dependencies
    @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 {
        // answer
        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 non protetto / JSON [intro-server-webjson-01] discusso nella Sezione 13.5.3;
  • righe 25–27: il metodo [getAllCategories] gestisce l'URL [/cors-getAllCategories] (riga 28) quando richiesto utilizzando il metodo HTTP [GET];
  • riga 26: il risultato del metodo [getAllCategories] verrà inviato al client. Questo risultato è un flusso JSON (l'attributo [produces] alla riga 27 e il tipo [String] del risultato alla riga 25);
  • riga 27: il metodo riceve gli stessi parametri del metodo [getAllCategories] del controller [MyControllerWithHttpOptions] che abbiamo appena esaminato;
  • riga 30: il metodo [myController.getAllCategories()] viene chiamato per inviare la risposta;

In definitiva, è il metodo [myController.getAllCategories()] del server non protetto che invia la risposta. Abbiamo semplicemente arricchito la sua risposta con le intestazioni richieste per le richieste cross-domain.

Questo viene fatto per tutti gli URL esposti dal server web/JSON non protetto [intro-server-webjson-01] discusso nella Sezione 13.5.3. Quando questo servizio espone l'URL [/url], la classe [MyControllerWithCors] sopra espone l'URL [/cors-url].

Una richiesta cross-domain procederà come segue:

  • il codice JavaScript del client richiede l'URL [/cors-url] utilizzando una richiesta HTTP GET o POST;
  • il browser che esegue questo codice intercetta la richiesta e richiede prima l'URL [/cors-url] utilizzando una richiesta HTTP OPTIONS per verificare che il servizio web di destinazione accetti richieste cross-origin;
  • uno dei metodi nel controller [MyControllerWithHttpOptions] invia le intestazioni cross-domain previste dal browser;
  • il browser richiede quindi l'URL iniziale [/cors-url] utilizzando una richiesta HTTP GET o POST;
  • uno dei metodi nel controller [MyControllerWithCors] risponde quindi;

18.9.6. Test

La classe di avvio per il 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]. Come risultato di questa configurazione, viene avviato il server Tomcat incorporato negli archivi del progetto e l'applicazione web [intro-spring-cors-server-jpa] viene distribuita su di esso. 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, alla fine vengono esposti due tipi di URL:
    • quelli del servizio web protetto: /url;
    • quelli del servizio web che accetta richieste cross-domain: /cors-url;

Ora siamo pronti per ulteriori test. Lanciamo la nuova versione del servizio web e scopriamo che il problema persiste. Non è cambiato nulla. Se aggiungiamo un'istruzione di output della console alla riga 7 qui sotto, questa non viene mai visualizzata, indicando 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) ;
        // headers CORS
        setHeaders(origin, httpServletResponse);
    }
 

Dopo alcune ricerche, scopriamo che, per impostazione predefinita, Spring MVC gestisce autonomamente le richieste HTTP [OPTIONS]. Pertanto, è sempre Spring a rispondere, e mai il metodo [getAllCategories] alla riga 5 sopra. 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 {
 
    // cross-domain queries
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
 
    @Autowired
    private DispatcherServlet dispatcherServlet;
 
    @PostConstruct
    public void init() {
        // the application processes requests itself 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] discusso nella sezione 13.5.3;
  • righe 28-29: il metodo [init] (riga 29) verrà eseguito non appena la classe [AppConfig] sarà stata istanziata e saranno state eseguite le iniezioni Spring. Pertanto, quando viene eseguito, il campo alla riga 26 è già stato inizializzato;
  • riga 31: configuriamo il bean [dispatcherServlet] in modo che consenta all'applicazione web di gestire autonomamente le richieste HTTP [OPTIONS];

Eseguiamo nuovamente i test con questa nuova configurazione. Otteniamo il seguente risultato:

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

Ora esaminiamo la seconda richiesta:

  • in [1], la richiesta in esame;
  • in [2], questa è la richiesta GET. Grazie alla prima richiesta [OPTIONS], il browser ha ricevuto le informazioni richieste. Ora sta eseguendo la richiesta [GET] inizialmente richiesta;
  • 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 del server [3] è normale [HTTP/1.1 200 OK]. Dovremmo quindi avere il documento richiesto. È possibile che il server abbia effettivamente inviato il documento ma che il browser ne impedisca l'uso perché richiede che, anche per la richiesta GET, la risposta includa l'intestazione HTTP [Access-Control-Allow-Origin:http://localhost:8081].

Modificheremo quindi il controller [MyControllerWithCors] in modo che invii anche le intestazioni richieste per le richieste cross-origin:


    @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 {
        // headers CORS
        setHeaders(origin, httpServletResponse);
        // answer
        return myController.getAllCategories();
}
  • riga 6: le intestazioni richieste per le richieste cross-domain sono incluse nella risposta;

Dopo questa modifica, i risultati sono i seguenti:

Abbiamo ottenuto con successo l'elenco delle categorie.

18.10. Gli altri URL [GET]

Nei controller [MyControllerWithCors, MyControllerWithHttpOptions], il codice delle azioni che gestiscono gli URL [GET] richiesti segue lo schema delle azioni che in precedenza gestivano l'URL [/cors-getAllCategories]. Il lettore può verificare il codice negli esempi forniti con questo documento. Ecco un esempio per l'URL [/cors-getAllProducts]:

in [MyControllerWithHttpOptions]


    @RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.OPTIONS)
    public void getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) {
        // headers 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 {
        // headers CORS
        setHeaders(origin, httpServletResponse);
        // answer
        return myController.getAllProduits();
}

Il risultato ottenuto è il seguente:

18.11. URL [POST]

Esaminiamo il seguente scenario:

  • Effettuiamo un POST [1] all'URL [2];
  • in [3], il valore inviato. Si tratta di una stringa JSON;
  • In generale, stiamo cercando di creare una categoria chiamata [category2];

Al momento non stiamo modificando alcun codice. Il risultato ottenuto è il seguente:

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

Modifichiamo il metodo [AbstractController.sendHeaders] come segue:


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;
 
    // sending options to the customer
    public void setHeaders(String origin, HttpServletResponse response) {
        // Cors allowed ?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // set header CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // certain headers are allowed
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // we authorize GET and POST
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
    }
}
  • riga 21: abbiamo aggiunto l'intestazione HTTP [Content-Type] (non fa differenza tra maiuscole e minuscole);
  • riga 23: abbiamo aggiunto il metodo HTTP [POST];

Ciò significa che i metodi [POST] vengono gestiti allo stesso modo delle richieste [GET]. Ecco un esempio dell'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 {
        // headers CORS
        setHeaders(origin, httpServletResponse);
        // answer
        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 {
        // headers CORS
        setHeaders(origin, httpServletResponse);
}

Il risultato è il seguente:

 

La categoria [categorie2] è stata aggiunta con successo al database. Il DBMS le ha assegnato la chiave primaria 1729.

18.12. Il [AuthenticateCorsController]

  

Il controller [AuthenticateCorsController] serve a fornire l'URL [/cors-authenticate], che consente di richiamare l'URL esistente [/authenticate] con una richiesta cross-domain. 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 {
        // headers CORS
        setHeaders(origin, response);
        // original method
        return authenticateController.authenticate();
    }
 
    @RequestMapping(value = "/cors-authenticate", method = RequestMethod.OPTIONS)
    public void corsAuthenticate(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        // headers CORS
        setHeaders(origin, response);
    }
 
}

Ecco due esempi:

  • Le risposte vengono visualizzate utilizzando il seguente codice JavaScript:

function doGet(url) {
    // make a manual Ajax call
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // text result
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            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. Conclusione

La nostra applicazione ora supporta le richieste cross-domain. Queste possono essere abilitate o disabilitate tramite la configurazione nella classe [AppConfig]:


@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {
 
    // cross-domain queries
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
 
    @Autowired
    private DispatcherServlet dispatcherServlet;
 
    @PostConstruct
    public void init() {
        // the application processes requests itself HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}