Skip to content

18. [Kurs]: Cross-Origin Resource Sharing

Stichwörter: CORS (Cross-Origin Resource Sharing).

Dieses Kapitel liegt etwas außerhalb des Rahmens des Tutorials. Es wurde aufgenommen, da es eine Einführung in die Webprogrammierung und die JavaScript-Programmierung bietet. Es ist wichtig zu beachten, dass eines der Ziele dieses Tutorials darin besteht, Konzepte vorzustellen, die in der JEE-Entwicklung, d. h. der Webentwicklung auf Basis von Java-Frameworks, häufig verwendet werden. Hier erweitern wir den im Rahmen der Produkt- und Kategoriedatenbank-Studie verwendeten Webserver, damit er domänenübergreifende Anfragen akzeptieren kann.

Im Dokument [AngularJS / Spring 4 Tutorial] entwickeln wir eine Client/Server-Anwendung, bei der der Client eine AngularJS-Anwendung ist:

  • Die HTML/CSS/JS-Seiten der Angular-Anwendung stammen vom Server [1];
  • in [2] sendet der [dao]-Dienst eine Anfrage an einen anderen Server, Server [2]. Nun, das wird vom Browser, auf dem die Angular-Anwendung läuft, unterbunden, da es sich um eine Sicherheitslücke handelt. Die Anwendung kann nur den Server abfragen, von dem sie stammt, d. h. Server [1];

Tatsächlich ist es unzutreffend zu sagen, dass der Browser die Angular-Anwendung daran hindert, Server [2] abzufragen. Er fragt diesen vielmehr an, ob er es einem Client, der nicht von ihm stammt, gestattet, ihn abzufragen. Diese Technik der gemeinsamen Nutzung wird als CORS (Cross-Origin Resource Sharing) bezeichnet. Server [2] erteilt die Erlaubnis, indem er bestimmte HTTP-Header sendet.

Wir werden die folgende Architektur erstellen:

  • In [1] liefert eine Webanwendung HTML/JS-Seiten;
  • in [2] führt der Browser das in die HTML-Seiten eingebettete JavaScript aus, um den sicheren Webdienst [3] abzufragen;

18.1. Support

  

Die Projekte zu diesem Kapitel finden Sie im Ordner [support / chap-18].

18.2. Das Client-Projekt

Erstellen Sie das folgende Eclipse-Projekt:

  

18.3. Maven-Konfiguration

Das Projekt ist ein Maven-Projekt mit der folgenden [pom.xml]-Datei:


<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>
  • Zeilen 11–15: Dies ist ein Spring-Boot-Projekt;
  • Zeilen 23–26: Wir verwenden die Abhängigkeit [spring-boot-starter-web], die einen Tomcat-Server und Spring MVC enthält;

18.4. Spring-Konfiguration

  

Die [WebConfig]-Klasse, die das Spring-Projekt konfiguriert, sieht wie folgt aus:


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/");
    }
}
  • Zeile 15: Die Klasse konfiguriert ein Spring-MVC-Projekt;
  • Zeile 16: Die Klasse erweitert die Klasse [WebMvcConfigurerAdapter], um einige ihrer Methoden zu überschreiben;
  • Zeilen 18–36: Diese Beans sind uns bereits begegnet, beispielsweise in Abschnitt 13.5.3.1. Beachten Sie in Zeile 35, dass der Webdienst auf Port 8081 läuft;
  • Zeilen 38–42: Mit der Methode [addResourceHandlers] können Sie statische Ressourcen definieren, d. h. Ressourcen, die nicht vom [DispatcherServlet] in Zeile 23 verarbeitet werden;
  • Zeile 40: Jede Anfrage nach einer Ressource mit der Endung .html gibt die in der Anfrage angeforderte Datei zurück, die sich im Ordner [static] des Klassenpfads des Projekts befindet;
  • Zeile 41: Jede Anfrage nach einer Ressource mit der Endung .js gibt die JavaScript-Datei zurück, die in der Anfrage angefordert wurde und sich im Ordner [static/js] des Klassenpfads des Projekts befindet;
  

18.5. Grundlagen von jQuery und JavaScript

Die HTML-Seite des Clients sieht wie folgt aus:

 

Sie enthält JavaScript-Code (JS), der im Browser ausgeführt wird. Wir werden einige Grundlagen von JavaScript behandeln, um den Code besser zu verstehen. Der Client stellt HTTP-Anfragen mithilfe der jQuery-Bibliothek [https://jquery.com/], die viele Funktionen bereitstellt, die die JavaScript-Entwicklung vereinfachen. Wir erstellen eine statische HTML-Datei [jQuery.html] und legen sie im Ordner [static] ab:

 

Diese Datei wird folgenden Inhalt haben:


<!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>
  • Zeile 6: jQuery wird importiert;
  • Zeilen 10–12: ein Seitenelement mit der ID [element1]. Wir werden mit diesem Element arbeiten.

Wir müssen die Datei [jquery-2.1.3.min.js] herunterladen. Die neueste Version von jQuery finden Sie unter der URL [http://jquery.com/download/]:

Image

Legen Sie die heruntergeladene Datei im Ordner [static/js] ab und passen Sie Zeile 6 der HTML-Datei an die installierte Version an.

Sobald das erledigt ist, öffnen Sie die statische Ansicht [jQuery.html] in Chrome [1-2]:

Drücke in Google Chrome [Strg-Umschalt-I], um die Entwicklertools zu öffnen [3]. Über die Registerkarte [Konsole] [4] kannst du JavaScript-Code ausführen. Nachfolgend findest du JavaScript-Befehle, die du eingeben kannst, sowie Erläuterungen zu ihrer Funktion.

JS
Ergebnis
$("#element1")
: gibt die Sammlung aller Elemente mit der ID [element1] zurück,
also normalerweise eine Sammlung von 0 oder 1 Element
, da es auf einer HTML-Seite keine zwei identischen IDs geben kann.
$("#element1").text("blabla")
: setzt den Text [blabla] für alle Elemente
in der Sammlung. Dies ändert den
auf der Seite angezeigten Inhalt
$("#element1").hide()
blendet die Elemente in der Sammlung aus.
Der Text [blabla] wird nicht mehr angezeigt.
$("#element1")
: zeigt die Sammlung wieder an. Dies
ermöglicht es uns zu sehen, dass das Element mit der ID [element1]
das CSS-Attribut style='display: none;', wodurch
das Element ausblendet.
$("#element1").show()
: zeigt die Elemente in der Sammlung an. Der Text
[blabla] erscheint wieder. Es ist das
style='display: block;', das diese
Anzeige.
$("#element1").attr('style','color: red')
: Legt ein Attribut für alle Elemente in der
Sammlung. Das Attribut lautet hier [style] und sein Wert
[color: red]. Der Text [blabla] wird rot.
Array
Wörterbuch

Beachten Sie, dass sich die URL des Browsers während all dieser Vorgänge nicht geändert hat. Es fand keine Kommunikation mit dem Webserver statt. Alles geschieht innerhalb des Browsers. Sehen wir uns nun den Quellcode der Seite an:

Dies ist der ursprüngliche Text. Er spiegelt die Änderungen nicht wider, die wir in den Zeilen 10–12 an dem Element vorgenommen haben. Das sollte man beim Debuggen von JavaScript unbedingt beachten. Daher ist es oft nicht notwendig, den Quellcode der angezeigten Seite einzusehen.

18.6. Der JavaScript-Code der Anwendung

Kehren wir zu der Seite der Client-Anwendung zurück, die den Webdienst / jSON abfragen wird:

  
 

Der HTML-Code für diese Seite lautet wie folgt:


<!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>
  • Zeile 6: Wir importieren die jQuery-Bibliothek;
  • Zeile 7: Wir importieren Code, den wir schreiben werden;
  • Zeilen 15, 19, 26, 29, 31: Beachten Sie die [id]-Bezeichner der Seitenkomponenten. Das JavaScript verweist über diese Bezeichner auf diese Komponenten;

Der [client.js]-Code lautet wie folgt:


// 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");
});
  • Zeilen 80–87: JavaScript-Code, der ausgeführt wird, nachdem das Dokument im Browser vollständig geladen wurde;
  • Zeilen 81–86: Ruft die Referenzen der verschiedenen Elemente im HTML-Dokument über ihre [id]-Identifikatoren ab;
  • Zeilen 2–9: Globale Variablen, auf die in allen in der JavaScript-Datei definierten Funktionen zugegriffen werden kann;
  • Zeile 13: Ruft die vom Benutzer eingegebene URL ab;
  • Zeile 14: Ruft den Wert ab, den der Benutzer senden möchte (leer bei einer GET-Operation);
  • Zeile 15: Ruft den vom Benutzer eingegebenen Benutzernamen ab;
  • Zeile 16: Ruft das Passwort ab;
  • Zeile 17: Ruft die Methode [get] oder [post] ab, die bei der Anfrage der URL aus Zeile 9 verwendet werden soll:
    • [document] bezieht sich auf das vom Browser geladene Dokument, bekannt als DOM (Document Object Model),
    • [document.forms[0]] bezieht sich auf das erste Formular im Dokument; ein Dokument kann mehrere Formulare enthalten. Hier gibt es nur eines,
    • [document.forms[0].elements['method']] bezieht sich auf das Formularelement mit dem Attribut [name='method']. Es gibt zwei:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • (Fortsetzung)
    • [document.forms[0].elements['method'].value] ist der Wert, der für die Komponente mit dem Attribut [name='method'] übermittelt wird. Wir wissen, dass der übermittelte Wert dem Wert des Attributs [value] des ausgewählten Optionsfelds entspricht. Hier handelt es sich also um eine der Zeichenfolgen ['get', 'post'];
  • Zeile 18: Wir erstellen die Base74-Kodierung der Zeichenkette `username:password`. Diese kodierte Zeichenkette wird im HTTP-Header [Authorization] verwendet, den wir an den Server senden, um die Anfrage zu authentifizieren;
  • Zeilen 22–26: Je nach der zu verwendenden HTTP-Methode führen wir die Methode [doGet] oder [doPost] aus;
  • Die jQuery-Methode [$.ajax] führt eine HTTP-Anfrage durch;
  • Zeilen 32–34: Wir kommunizieren mit einem Server, der einen HTTP-Header [Authorization: Basic code] erfordert;
  • Zeile 35: Der Benutzer gibt URLs der Form [/cors-getAllCategories,/cors-addProduits, ...] ein. Diese URLs müssen daher um die Server-URL aus Zeile 6 ergänzt werden;
  • Zeile 36: Zu verwendende HTTP-Methode;
  • Zeile 37: Der Server gibt JSON zurück. Wir geben den Typ [text] als Ergebnistyp an, um die Daten genau so anzuzeigen, wie sie empfangen wurden;
  • Zeile 42: Anzeige der Textantwort des Servers;
  • Zeilen 48–49: Anzeige einer etwaigen Fehlermeldung;
  • Zeile 53: Die Methode [doPost] erhält einen zweiten Parameter, nämlich den zu übermittelnden Wert;
  • Zeile 61: um anzugeben, dass der gesendete Wert in Form einer JSON-Zeichenkette vorliegt;

18.7. Client-Ausführung

Die Client-Anwendung ist eine Spring Boot-Anwendung, die von der folgenden ausführbaren [Boot]-Klasse gestartet wird:

  

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);
    }
}
  • Zeile 10: Die Methode [SpringApplication.run] verwendet die Konfigurationsdatei [WebConfig]. Die Seite [client.html] wird auf dem Tomcat-Server bereitgestellt, der sich im Klassenpfad des Projekts befindet;

18.8. Die URL [/getAllCategories]

Wir starten:

  • den Web-/JSON-Server auf Port 8080;
  • den Client für diesen Server auf Port 8081;

dann fordern wir die URL [http://localhost:8081/client.html] [1] an:

  • in [2] führen wir eine GET-Anfrage an die URL [http://localhost:8080/getAllCategories] durch;

Wir erhalten keine Antwort vom Server. Wenn wir uns die Chrome-Entwicklerkonsole (Strg-Umschalt-I) ansehen, wird ein Fehler angezeigt:

  • In [1] befinden wir uns auf der Registerkarte [Netzwerk];
  • In [2] sehen wir, dass die HTTP-Anfrage nicht [GET], sondern [OPTIONS] lautet. Bei einer domänenübergreifenden Anfrage überprüft der Browser durch Senden einer [OPTIONS]-HTTP-Anfrage beim Server, ob bestimmte Bedingungen erfüllt sind. In diesem Fall handelt es sich um die durch die Markierungen [5-6] gekennzeichneten Anfragen;
  • In [5] fragt der Browser, ob die Ziel-URL mit einem GET erreicht werden kann. Der Anforderungsheader [Access-Control-Request-Method] fordert eine Antwort mit einem HTTP-Header [Access-Control-Allow-Methods] an, der angibt, dass die angeforderte Methode akzeptiert wird;
  • in [6] sendet der Browser den HTTP-Header [Origin: http://localhost:8081]. Dieser Header fordert eine Antwort in einem HTTP-Header [Access-Control-Allow-Origin] an, der angibt, dass die angegebene Herkunft akzeptiert wird;
  • In [7] fragt der Browser ab, ob die HTTP-Header [Accept] und [Authorization] akzeptiert werden. Der Request-Header [Access-Control-Request-Headers] erwartet eine Antwort mit einem HTTP-Header [Access-Control-Allow-Headers], der angibt, dass die angeforderten Header akzeptiert werden;
  • In [3] tritt ein Fehler auf. Ein Klick auf das Symbol führt zu Fehler [4];
  • In [4] weist die Meldung darauf hin, dass der Server den HTTP-Header [Access-Control-Allow-Origin] nicht gesendet hat, der angibt, ob der Ursprung der Anfrage akzeptiert wird;
  • In [8] sehen wir, dass der Server diesen Header tatsächlich nicht gesendet hat. Infolgedessen hat der Browser die ursprünglich angeforderte HTTP-GET-Anfrage abgelehnt;

Wir müssen den Webserver / JSON anpassen.

18.9. Der neue Webservice / json

Wir erstellen ein neues Maven-Projekt [intro-spring-cors-server-jpa]:

18.9.1. Maven-Konfiguration

Die Maven-Konfiguration für den neuen Webdienst lautet wie folgt:


<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>
  • Zeilen 23–27: Wir rufen alle Daten aus der bisherigen Arbeit ab, indem wir auf das /json-Archiv des sicheren Webservers zugreifen;

18.9.2. Spring-Konfiguration

Die Konfigurationsklasse [AppConfig] sieht wie folgt aus:

  

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;
    }
}
  • Zeile 10: Die Klasse ist eine Spring-Konfigurationsklasse;
  • Zeile 11: Weitere Spring-Komponenten finden sich im Paket [spring.cors.server.service];
  • Zeilen 16–19: Wir erstellen eine Spring-Komponente namens [isCorsEnabled], die angibt, ob Clients außerhalb der Domäne des Servers akzeptiert werden oder nicht;

18.9.3. Die Klasse [AbstractCorsController]

Die Klasse [AbstractCorsController], die die übergeordnete Klasse aller Controller in dieser Anwendung sein wird:

 

Der Code lautet wie folgt:


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");
    }
}
  • Zeile 7: Die Klasse [CorsController] ist abstrakt, da sie dazu dient, erweitert und nicht instanziiert zu werden;
  • Zeilen 13–24: Die Methode [setHeaders] fügt die für domänenübergreifende Anfragen erforderlichen HTTP-Header zur [HttpServletResponse response] (Zeile 13) hinzu, die an den Client gesendet wird;
  • Zeile 33: Die Methode [setHeaders] akzeptiert als Parameter:
    • die Zeichenkette [origin], die im HTTP-Header [Origin] von domänenübergreifenden Anfragen enthalten ist:
Origin:http://localhost:8081

Hier hätte der Parameter [origin] in Zeile 13 den Wert [http://localhost:8081]. Wenn die Anfrage den HTTP-Header [Origin] nicht enthält, stellen wir sicher, dass [origin==null] gilt;

  • (Fortsetzung)
    • das [HttpServletResponse response]-Objekt, das an den Client zurückgegeben wird, der die Anfrage gestellt hat;

Diese beiden Parameter werden von Spring injiziert;

  • Zeilen 15–175: Wenn die Anwendung so konfiguriert ist, dass sie domänenübergreifende Anfragen akzeptiert, und wenn der Absender den HTTP-Header [Origin] gesendet hat, und wenn diese Herkunft mit [http://localhost] beginnt, wird die domänenübergreifende Anfrage akzeptiert; andernfalls wird sie abgelehnt;
  • Zeile 19: Befindet sich der Client in der Domäne [http://localhost:port], senden wir den HTTP-Header:

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

was bedeutet, dass der Server die Herkunft des Clients akzeptiert;

  • Zeile 21: Wir haben in der [OPTIONS]-HTTP-Anfrage zwei spezifische HTTP-Header angegeben:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

Als Antwort auf den HTTP-Header [Access-Control-Request-X] sendet der Server einen HTTP-Header [Access-Control-Allow-X] zurück, der angibt, was erlaubt ist. Die Zeilen 20–23 wiederholen lediglich die Anfrage des Clients, um anzuzeigen, dass sie akzeptiert wurde;

18.9.4. Der Controller [MyControllerWithHttpOptions]

Um zu vermeiden, dass der in Abschnitt 13.5.3 besprochene unsichere Web-/JSON-Server [intro-server-webjson-01] geändert werden muss, erstellen wir einen neuen Controller. Während der unsichere Server die URL [/url] verarbeitet, wird der neue Controller die URL [/cors-url] verarbeiten, und diese URL akzeptiert Cross-Origin-Anfragen.

Die Klasse [MyControllerWithHttpOptions] ist der Controller, der HTTP-Anfragen vom Typ [OPTIONS] verarbeitet:

 

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);
    }
...
  • Zeile 14: Die Klasse ist ein Spring-MVC-Controller;
  • Zeile 15: Die Klasse [MyControllerWithHttpOptions] erweitert die soeben beschriebene Klasse [AbstractCorsController];
  • Zeilen 17–18: Die Methode [getAllCategories] (Zeile 18) verarbeitet die URL ["/cors-getAllCategories"], wenn sie mit der HTTP-Methode [OPTIONS] angefordert wird;
  • Zeile 18: Die Methode [getAllCategories] akzeptiert zwei Parameter:
    • [@RequestHeader(value = "Origin", required = false) String origin], um den Wert des HTTP-Headers [Origin:http://localhost:8081] abzurufen, sofern dieser vorhanden ist. In diesem Beispiel erhält der Parameter [String origin] den Wert [http://localhost:8081]. Dieser Header ist nicht erforderlich [required = false]. Wenn er nicht vorhanden ist, hat der Parameter [String origin] den Wert null;
    • [HttpServletResponse httpServletResponse]: die Antwort, die an den Client gesendet wird;
  • Zeile 21: Wir senden die HTTP-Header, die Cross-Origin-Anfragen ermöglichen. Die Methode [setHeaders] ist in der übergeordneten Klasse [AbstractCorsController] definiert;

Dies geschieht für alle URLs, die vom ungesicherten Web-/JSON-Server [intro-server-webjson-01] bereitgestellt werden, der in Abschnitt 13.5.3 behandelt wurde. Wenn dieser Dienst die URL [/url] bereitstellt, stellt die oben genannte Klasse [MyControllerWithHttpOptions] die URL [/cors-url] bereit.

18.9.5. Der Controller [MyControllerWithCors]

 

Die Klasse [MyControllerWithCors] ist der Controller, der [GET]- und [POST]-HTTP-Anfragen verarbeitet:


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();
    }
...
  • Zeile 17: Die Klasse [MyControllerWithCors] ist ein Spring-MVC-Controller
  • Zeile 18: Sie erweitert die Klasse [AbstractCorsController];
  • Zeilen 21–22: Einbindung des Controllers [MyController] aus dem ungesicherten Webserver / JSON [intro-server-webjson-01], der in Abschnitt 13.5.3 behandelt wurde;
  • Zeilen 25–27: Die Methode [getAllCategories] verarbeitet die URL [/cors-getAllCategories] (Zeile 28), wenn sie mit der HTTP-Methode [GET] angefordert wird;
  • Zeile 26: Das Ergebnis der Methode [getAllCategories] wird an den Client gesendet. Dieses Ergebnis ist ein JSON-Stream (das Attribut [produces] in Zeile 27 und der Typ [String] des Ergebnisses in Zeile 25);
  • Zeile 27: Die Methode erhält dieselben Parameter wie die Methode [getAllCategories] des Controllers [MyControllerWithHttpOptions], den wir gerade betrachtet haben;
  • Zeile 30: Die Methode [myController.getAllCategories()] wird aufgerufen, um die Antwort zu senden;

Letztendlich ist es die Methode [myController.getAllCategories()] des ungesicherten Servers, die die Antwort sendet. Wir haben lediglich die Antwort um die für domänenübergreifende Anfragen erforderlichen Header erweitert.

Dies geschieht für alle URLs, die vom ungesicherten Web/JSON-Server [intro-server-webjson-01] bereitgestellt werden, der in Abschnitt 13.5.3 behandelt wurde. Wenn dieser Dienst die URL [/url] bereitstellt, stellt die oben genannte Klasse [MyControllerWithCors] die URL [/cors-url] bereit.

Eine domänenübergreifende Anfrage läuft wie folgt ab:

  • Der JavaScript-Code des Clients fordert die URL [/cors-url] mittels einer HTTP-GET- oder -POST-Anfrage an;
  • Der Browser, der diesen Code ausführt, fängt die Anfrage ab und fordert zunächst die URL [/cors-url] mittels einer HTTP-OPTIONS-Anfrage an, um zu überprüfen, ob der Ziel-Webdienst domänenübergreifende Anfragen akzeptiert;
  • eine der Methoden im Controller [MyControllerWithHttpOptions] sendet die vom Browser erwarteten domänenübergreifenden Header;
  • Der Browser fordert dann die ursprüngliche URL [/cors-url] mit einer HTTP-GET- oder POST-Anfrage an;
  • eine der Methoden im Controller [MyControllerWithCors] antwortet daraufhin;

18.9.6. Tests

Die Boot-Klasse für das Projekt [intro-spring-cors-server-jpa] lautet wie folgt:

  

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);
    }
}
  • Zeile 10: Die statische Methode [SpringApplication.run] wird mit der Spring-Konfiguration [AppConfig] ausgeführt. Als Ergebnis dieser Konfiguration wird der in den Projektarchiven eingebettete Tomcat-Server gestartet und die Webanwendung [intro-spring-cors-server-jpa] darauf bereitgestellt. Die Webanwendung des ungesicherten Servers [intro-server-webjson-01], die Teil der Projektarchive ist, wird ebenfalls darauf bereitgestellt. Da das Projekt [intro-spring-security-server-01] ebenfalls Teil der Archive ist, werden letztendlich zwei Arten von URLs bereitgestellt:
    • die des sicheren Webdienstes: /url;
    • die des Webdienstes, der domänenübergreifende Anfragen akzeptiert: /cors-url;

Wir sind nun bereit für weitere Tests. Wir starten die neue Version des Webdienstes und stellen fest, dass das Problem weiterhin besteht. Es hat sich nichts geändert. Wenn wir in Zeile 7 unten eine Konsolenausgabeanweisung hinzufügen, wird diese nie angezeigt, was darauf hindeutet, dass die Methode [getAllCategories] der Klasse [MyControllerWithHttpOptions] nie aufgerufen wird;


@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);
    }
 

Nach einigen Recherchen stellen wir fest, dass Spring MVC standardmäßig HTTP-[OPTIONS]-Anfragen selbst verarbeitet. Daher antwortet immer Spring und niemals die Methode [getAllCategories] in Zeile 5 oben. Dieses Standardverhalten von Spring MVC lässt sich ändern. Wir ändern die vorhandene Klasse [AppConfig]:

  

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);
    }
}
  • Zeilen 25–26: Injektion der [dispatcherServlet]-Bean, die Client-Anfragen verarbeitet. Diese Bean wurde in der Konfiguration des ungesicherten Web-/JSON-Servers [intro-server-webjson-01] definiert, der in Abschnitt 13.5.3 behandelt wurde;
  • Zeilen 28–29: Die [init]-Methode (Zeile 29) wird ausgeführt, sobald die [AppConfig]-Klasse instanziiert und die Spring-Injektionen durchgeführt wurden. Bei ihrer Ausführung ist das Feld in Zeile 26 daher bereits initialisiert;
  • Zeile 31: Wir konfigurieren die [dispatcherServlet]-Bean so, dass die Webanwendung [OPTIONS]-HTTP-Anfragen selbst bearbeiten kann;

Wir führen die Tests mit dieser neuen Konfiguration erneut durch. Wir erhalten das folgende Ergebnis:

  • In [1] sehen wir, dass zwei HTTP-Anfragen an die URL [http://localhost:8080/cors-getAllCategories] gestellt werden;
  • in [2] die [OPTIONS]-Anfrage;
  • in [3] die drei HTTP-Header, die wir gerade in der Antwort des Servers konfiguriert haben;

Betrachten wir nun die zweite Anfrage:

  • in [1] die untersuchte Anfrage;
  • in [2] handelt es sich um die GET-Anfrage. Dank der ersten [OPTIONS]-Anfrage hat der Browser die angeforderten Informationen erhalten. Er führt nun die ursprünglich angeforderte [GET]-Anfrage aus;
  • in [3] die Antwort des Servers;
  • in [4] sendet der Server JSON;
  • in [5] ist ein Fehler aufgetreten;
  • in [6] die Fehlermeldung;

Es ist schwieriger zu erklären, was hier passiert ist. Die Antwort des Servers [3] ist normal [HTTP/1.1 200 OK]. Wir sollten daher über das angeforderte Dokument verfügen. Es ist möglich, dass der Server das Dokument tatsächlich gesendet hat, der Browser jedoch dessen Nutzung verhindert, da er verlangt, dass die Antwort auch für die GET-Anfrage den HTTP-Header [Access-Control-Allow-Origin:http://localhost:8081] enthält.

Wir werden nun den Controller [MyControllerWithCors] so ändern, dass er auch die für Cross-Origin-Anfragen erforderlichen Header sendet:


    @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();
}
  • Zeile 6: Die für domänenübergreifende Anfragen erforderlichen Header sind in der Antwort enthalten;

Nach dieser Änderung sind die Ergebnisse wie folgt:

Wir haben die Liste der Kategorien erfolgreich abgerufen.

18.10. Die anderen [GET]-URLs

In den Controllern [MyControllerWithCors, MyControllerWithHttpOptions] folgt der Code für die Aktionen, die die angeforderten [GET]-URLs verarbeiten, dem Muster der Aktionen, die zuvor die URL [/cors-getAllCategories] verarbeitet haben. Der Leser kann den Code in den mit diesem Dokument bereitgestellten Beispielen überprüfen. Hier ist ein Beispiel für die 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();
}

Das Ergebnis lautet wie folgt:

18.11. [POST]-URLs

Betrachten wir das folgende Szenario:

  • Wir senden einen POST-Request [1] an die URL [2];
  • in [3] den gesendeten Wert. Dies ist eine JSON-Zeichenkette;
  • Insgesamt versuchen wir, eine Kategorie namens [category2] zu erstellen;

Wir nehmen derzeit keine Änderungen am Code vor. Das Ergebnis sieht wie folgt aus:

  • In [1] wird, wie bei [GET]-Anfragen, eine [OPTIONS]-Anfrage vom Browser gestellt;
  • in [2] fordert er die Zugriffsberechtigung für eine [POST]-Anfrage an. Zuvor war es [GET];
  • in [3] wird die Autorisierung zum Senden der HTTP-Header [accept, authorization, content-type] angefordert. Zuvor hatten wir nur die ersten beiden Header;
  • in [4] gewährt der Webdienst nicht alle angeforderten Berechtigungen, was zu Fehler [5] führt;

Wir ändern die Methode [AbstractController.sendHeaders] wie folgt:


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");
    }
}
  • Zeile 21: Wir haben den HTTP-Header [Content-Type] hinzugefügt (Groß-/Kleinschreibung spielt keine Rolle);
  • Zeile 23: Wir haben die HTTP-Methode [POST] hinzugefügt;

Das bedeutet, dass [POST]-Methoden genauso behandelt werden wie [GET]-Anfragen. Hier ist ein Beispiel für die 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);
}

Das Ergebnis lautet wie folgt:

 

Die Kategorie [categorie2] wurde erfolgreich zur Datenbank hinzugefügt. Das DBMS hat ihr den Primärschlüssel 1729 zugewiesen.

18.12. Der [AuthenticateCorsController]

  

Der Controller [AuthenticateCorsController] dient dazu, die URL [/cors-authenticate] bereitzustellen, über die Sie die vorhandene URL [/authenticate] mit einer domänenübergreifenden Anfrage aufrufen können. Sein Code lautet wie folgt:


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);
    }
 
}

Hier sind zwei Beispiele:

  • Die Antworten werden mithilfe des folgenden JavaScript-Codes angezeigt:

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()));
        }
    })
}
  • Die Antwort [1] wird in Zeile 14 der Funktion [success] angezeigt;
  • Die Antwort [2] wird durch Zeile 20 der Funktion [error] angezeigt. Die Funktion [JSON.stringify] erstellt die JSON-Zeichenkette des Objekts [jqXHR.statusCode()], welches den aufgetretenen Fehler kapselt. Dieses Objekt liefert nur wenige Informationen. Es ist möglich, andere Methoden des Objekts [jqXHR] zu verwenden, um beispielsweise die vom Server zurückgegebenen HTTP-Header abzurufen;

18.13. Fazit

Unsere Anwendung unterstützt nun domänenübergreifende Anfragen. Diese können über die Konfiguration in der Klasse [AppConfig] aktiviert oder deaktiviert werden:


@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);
    }
}