Skip to content

18. [Cours]: Zarządzanie dostępem międzydomenowym

Słowa kluczowe: CORS (Cross-Origin Resource Sharing).

Ten rozdział nieco odbiega od tematyki TD. Został jednak zachowany, ponieważ wprowadza w programowanie internetowe i programowanie w języku JavaScript. Należy tutaj pamiętać, że jednym z celów niniejszego TD jest przedstawienie pojęć często wykorzystywanych w tworzeniu aplikacji JEE, tj. tworzeniu aplikacji internetowych opartych na frameworkach Java. W tym miejscu uzupełniamy serwer internetowy wykorzystywany w analizie bazy danych produktów i kategorii, aby umożliwić mu obsługę żądań międzydomenowych.

W dokumencie [Tutoriel AngularJS / Spring 4] opracowujemy aplikację klient-serwer, w której klientem jest aplikacja AngularJS:

  • strony HTML / CSS / JS aplikacji Angular pochodzą z serwera [1];
  • w [2] usługa [dao] wysyła żądanie do innego serwera, serwera [2]. Jest to jednak zabronione przez przeglądarkę, w której uruchomiona jest aplikacja Angular, ponieważ stanowi to lukę w zabezpieczeniach. Aplikacja może wysyłać zapytania wyłącznie do serwera, z którego pochodzi, czyli do serwera [1];

W rzeczywistości nieprawdziwe jest stwierdzenie, że przeglądarka uniemożliwia aplikacji Angular wysyłanie zapytań do serwera [2]. Aplikacja wysyła do niego zapytanie, aby dowiedzieć się, czy serwer zezwala na wysyłanie zapytań przez klienta spoza własnej domeny. Technikę tę nazywa się CORS (Cross-Origin Resource Sharing). Serwer [2] wyraża zgodę, wysyłając odpowiednie nagłówki HTTP.

Stworzymy następującą architekturę:

  • w [1] aplikacja internetowa dostarcza strony HTML / jS;
  • w [2] przeglądarka wykonuje kod JavaScript osadzony na stronach HTML w celu wysłania zapytania do zabezpieczonej usługi internetowej [3];

18.1. Support

  

Projekty z tego rozdziału znajdują się w folderze [support / chap-18].

18.2. Projekt klienta

Tworzymy następujący projekt w Eclipse:

  

18.3. Konfiguracja Maven

Projekt jest projektem Maven z następującym plikiem: [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>
  • wiersze 11–15: jest to projekt Spring Boot;
  • wiersze 23–26: wykorzystuje się zależność [spring-boot-starter-web], która zawiera serwer Tomcat oraz Spring MVC;

18.4. Konfiguracja Spring

  

Klasa [WebConfig], która konfiguruje projekt Spring, wygląda następująco:


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 {

    // -------------------------------- konfiguracja warstwy [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/");
    }
}
  • wiersz 15: klasa konfiguruje projekt Spring MVC;
  • wiersz 16: klasa ta dziedziczy po klasie [WebMvcConfigurerAdapter] w celu przedefiniowania niektórych jej metod;
  • wiersze 18–36: te bean’y znamy już z innych miejsc, na przykład z paragrafu 13.5.3.1. Należy zauważyć, że w wierszu 35 usługa internetowa będzie działać na porcie 8081;
  • wiersze 38–42: metoda [addResourceHandlers] pozwala definiować zasoby statyczne, tj. zasoby nieprzetwarzane przez metodę [DispatcherServlet] z wiersza 23;
  • wiersz 40: każde żądanie zasobu z rozszerzeniem .html spowoduje zwrócenie pliku, o który prosi żądanie, znajdującego się w folderze [static] w ścieżce Classpath projektu;
  • wiersz 41: każde żądanie zasobu z rozszerzeniem .js spowoduje zwrócenie pliku JavaScript, o który poproszono w żądaniu i który znajduje się w folderze [static/js] w ścieżce Classpath projektu;
  

18.5. Podstawy jQuery i JavaScript

Strona HTML klienta będzie wyglądać następująco:

 

Będzie ona zawierała kod JavaScript (jS) wykonywany w przeglądarce. Przedstawimy kilka podstaw JavaScriptu, które pozwolą nam zrozumieć ten kod. Klient będzie wysyłał wywołania HTTP przy użyciu biblioteki jQuery [https://jquery.com/], która udostępnia wiele funkcji ułatwiających tworzenie kodu JavaScript. Tworzymy plik statyczny HTML [jQuery.html], który umieszczamy w folderze [static]:

 

Plik ten będzie miał następującą zawartość:


<!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>
  • wiersz 6: import pliku jQuery;
  • wiersze 10–12: element strony o identyfikatorze [element1]. Będziemy pracować z tym elementem.

Musimy pobrać plik [jquery-2.1.3.min.js]. Najnowszą wersję pliku jQuery znajdziemy pod adresem URL [http://jquery.com/download/]:

Image

Pobrany plik umieścimy w folderze [static / js] i zmienimy wiersz 6 pliku HTML zgodnie z zainstalowaną wersją.

Po wykonaniu tych czynności wywołujemy widok statyczny [jQuery.html] za pomocą przeglądarki Chrome [1-2]:

W przeglądarce Google Chrome należy wprowadzić [Ctrl-Maj-I], aby wyświetlić narzędzia programistyczne [3]. Zakładka [Console] [4] umożliwia uruchamianie kodu JavaScript. Poniżej podajemy polecenia JavaScript, które należy wpisać, wraz z ich objaśnieniami.

JS
résultat
$("#element1")
: tworzy zbiór wszystkich elementów o identyfikatorze [element1],
czyli zazwyczaj zbiór zawierający 0 lub 1 element
ponieważ na stronie HTML nie mogą występować dwa identyczne identyfikatory.
$("#element1").text("blabla")
: przypisuje tekst [blabla] do wszystkich elementów
w kolekcji. Powoduje to zmianę
treści wyświetlanej na stronie
$("#element1").hide()
ukrywa elementy kolekcji.
Tekst [blabla] nie jest już wyświetlany.
$("#element1")
: ponownie wyświetla kolekcję. Dzięki temu
pozwala stwierdzić, że element o identyfikatorze [element1] ma
atrybut CSS style='display: none;', który powoduje,
sprawia, że element jest ukryty.
$("#element1").show()
: wyświetla elementy kolekcji. Tekst
[blabla] pojawia się ponownie. To właśnie atrybut
CSS style='display: block;' zapewnia taki
wyświetlanie.
$("#element1").attr('style','color: red')
: ustawia atrybut dla wszystkich elementów
kolekcji. Atrybutem tym jest tutaj [style], a jego wartość
[color: red]. Tekst [blabla] zmienia kolor na czerwony.
Tableau
Dictionnaire

Warto zauważyć, że identyfikator URL przeglądarki nie uległ zmianie podczas wszystkich tych operacji. Nie doszło do żadnej komunikacji z serwerem WWW. Wszystko odbywa się wewnątrz przeglądarki. Teraz przyjrzyjmy się kodowi źródłowemu strony:

To jest tekst początkowy. Nie odzwierciedla on w żaden sposób zmian, które wprowadziliśmy w elemencie w wierszach 10–12. Należy o tym pamiętać podczas debugowania kodu JavaScript. W takich przypadkach przeglądanie kodu źródłowego wyświetlanej strony jest często bezcelowe.

18.6. Kod JavaScript aplikacji

Wróćmy do strony aplikacji klienckiej, która będzie wysyłać zapytanie do serwisu internetowego / jSON:

  
 

Kod HTML tej strony wygląda następująco:


<!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">
        <!-- identyfikator -->
        Identifiant :
        <!--  -->
        <input type="text" id="identifiant" name="identifiant" value="" />
        <!--  hasło -->
        <br /> <br /> Mot de passe :
        <!--  -->
        <input type="text" id="password" name="password" value="" />
        <!--  metoda 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 />
        <!-- wprowadzona wartość -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted"
            size="50" />
        <!-- przycisk zatwierdzenia -->
        <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>
  • wiersz 6: importujemy bibliotekę jQuery;
  • wiersz 7: importujemy kod, który zamierzamy napisać;
  • wiersze 15, 19, 26, 29, 31: należy zanotować identyfikatory [id] elementów strony. Kod JavaScript odwołuje się do tych elementów za pomocą tych identyfikatorów;

Kod [client.js] wygląda następująco:


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

function requestServer() {
    // pobieramy informacje
    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);
    // kasujemy poprzednią odpowiedź
    response.text("");
    // ręczne wywołanie Ajax
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // wykonywanie ręcznego wywołania Ajax
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // wynik tekstowy
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // błąd systemowy
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}

function doPost(url, posted) {
    // wywołujemy ręcznie Ajax
    $.ajax({
        headers : {
            'Autoryzacja: 'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'POST',
        contentType : 'application/json; charset=UTF-8',
        data : posted,
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // wynik tekstowy
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // błąd systemowy
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}

// podczas ładowania dokumentu
$(document).ready(function() {
    // pobieramy identyfikatory elementów strony
    identifiant = $("#identifiant");
    password = $("#password");
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • wiersze 80–87: kod jS wykonywany po zakończeniu ładowania dokumentu w przeglądarce;
  • wiersze 81–86: pobierane są odniesienia do różnych elementów dokumentu HTML za pomocą ich identyfikatorów [id];
  • wiersze 2–9: zmienne globalne znane we wszystkich funkcjach zdefiniowanych w pliku jS;
  • wiersz 13: pobieramy wartość URL wprowadzoną przez użytkownika;
  • wiersz 14: pobierana jest wartość, którą użytkownik chce opublikować (pusta w przypadku operacji GET);
  • wiersz 15: pobieramy identyfikator wprowadzony przez użytkownika;
  • wiersz 16: pobieramy hasło użytkownika;
  • wiersz 17: pobieramy sposób [get] lub [post], który należy zastosować, aby zażądać URL z wiersza 9:
    • [document] oznacza dokument załadowany przez przeglądarkę, tzw. DOM (Document Object Model),
    • [document.forms[0]] oznacza pierwszy formularz w dokumencie; dokument może zawierać kilka formularzy. W tym przypadku jest tylko jeden,
    • [document.forms[0].elements['method']] oznacza element formularza, który posiada atrybut [name='method']. Są dwa takie elementy:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • (ciąg dalszy)
    • [document.forms[0].elements['method'].value] to wartość, która zostanie przesłana dla komponentu posiadającego atrybut [name='method']. Wiadomo, że wysyłana wartość to wartość atrybutu [value] zaznaczonego przycisku opcji. W tym przypadku będzie to więc jeden z ciągów znaków ['get', 'post'];
  • wiersz 18: tworzymy kodowanie Base74 ciągu znaków identyfikator:hasło. Ten zakodowany ciąg znaków zostanie wykorzystany w nagłówku HTTP [Authorization], który wyślemy na serwer w celu uwierzytelnienia żądania;
  • wiersze 22–26: w zależności od metody HTTP, która ma zostać użyta, wywołujemy metodę [doGet] lub [doPost];
  • metoda jQuery lub [$.ajax] wykonuje wywołanie HTTP;
  • wiersze 32–34: nawiązuje się połączenie z serwerem, który wymaga nagłówka HTTP lub [Authorization: Basic code];
  • wiersz 35: użytkownik wprowadzi dane typu URL o formacie [/cors-getAllCategories,/cors-addProduits, ...]. Należy zatem uzupełnić te dane o identyfikator serwera z wiersza 6;
  • wiersz 36: należy zastosować metodę HTTP;
  • wiersz 37: serwer zwraca jSON. Jako typ wyniku podaje się [text], aby wyświetlić go w takiej postaci, w jakiej został odebrany;
  • wiersz 42: wyświetlenie tekstowej odpowiedzi serwera;
  • wiersze 48–49: wyświetlenie ewentualnego komunikatu o błędzie;
  • wiersz 53: metoda [doPost] otrzymuje drugi parametr, którym jest wartość do wysłania;
  • wiersz 61: w celu wskazania, że wysyłana wartość będzie miała postać ciągu znaków jSON;

18.7. Działanie klienta

Aplikacja kliencka to aplikacja Spring Boot uruchamiana przez następującą klasę wykonywalną [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);
    }
}
  • wiersz 10: metoda [SpringApplication.run] korzysta z pliku konfiguracyjnego [WebConfig]. Strona [client.html] zostanie wdrożona na serwerze Tomcat znajdującym się w ścieżce Classpath projektu;

18.8. L'URL [/getAllCategories]

Uruchamiamy:

  • serwer WWW / JSON na porcie 8080;
  • klienta tego serwera na porcie 8081;

następnie wysyłamy żądanie do URL [http://localhost:8081/client.html] [1]:

  • w [2], wykonujemy GET na URL [http://localhost:8080/getAllCategories];

Nie otrzymujemy odpowiedzi z serwera. Po sprawdzeniu konsoli programistycznej przeglądarki Chrome (Ctrl-Shift-I) widzimy błąd:

  • w [1], znajdujemy się w zakładce [Network];
  • w zakładce [2] widać, że wysłane żądanie HTTP nie jest [GET], ale [OPTIONS]. W przypadku żądania międzydomenowego przeglądarka sprawdza u serwera, czy spełnionych jest pewna liczba warunków, wysyłając do niego żądanie o numerze HTTP [OPTIONS]. W tym przypadku żądania to te wskazane przez kropki [5-6];
  • w przypadku [5] przeglądarka pyta, czy cel URL jest dostępny za pośrednictwem GET. Nagłówek żądania [Access-Control-Request-Method] zawiera prośbę o odpowiedź z nagłówkiem HTTP [Access-Control-Allow-Methods] wskazującym, że żądana metoda jest akceptowana;
  • w odpowiedzi [6] przeglądarka wysyła nagłówek HTTP [Origin: http://localhost:8081]. Nagłówek ten zawiera prośbę o odpowiedź w postaci nagłówka HTTP [Access-Control-Allow-Origin], wskazującego, że wskazane źródło zostało zaakceptowane;
  • w nagłówku [7] przeglądarka pyta, czy nagłówki HTTP, [accept] i [authorization] są akceptowane. Nagłówek żądania [Access-Control-Request-Headers] oczekuje odpowiedzi z nagłówkiem HTTP lub [Access-Control-Allow-Headers], wskazującym, że żądane nagłówki są akceptowane;
  • w przypadku [3] pojawia się błąd. Po kliknięciu ikony pojawia się błąd [4];
  • w [4] komunikat wskazuje, że serwer nie wysłał nagłówka HTTP [Access-Control-Allow-Origin], który określa, czy źródło żądania jest akceptowane;
  • w [8] widać, że serwer rzeczywiście nie wysłał tego nagłówka. W rezultacie przeglądarka odmówiła wykonania żądania HTTP GET, o które pierwotnie proszono;

Musimy zmodyfikować serwer WWW / jSON.

18.9. Nowa usługa internetowa / json

Tworzymy nowy projekt Maven o nazwie [intro-spring-cors-server-jpa]:

18.9.1. Konfiguracja Maven

Konfiguracja Maven nowej usługi internetowej wygląda następująco:


<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>

    <!-- wtyczki -->
    <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>
  • wiersze 23–27: wykorzystujemy dotychczasowe osiągnięcia, opierając się na archiwum bezpiecznego serwera internetowego / json;

18.9.2. Konfiguracja Spring

Klasa konfiguracyjna [AppConfig] wygląda następująco:

  

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 {

    // żądania międzydomenowe
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
}
  • wiersz 10: klasa ta jest klasą konfiguracyjną Spring;
  • wiersz 11: pozostałe komponenty Springa należy szukać w pakiecie [spring.cors.server.service];
  • wiersze 16–19: tworzymy komponent Spring o nazwie [isCorsEnabled], który określa, czy akceptujemy klientów spoza domeny serwera, czy nie;

18.9.3. Klasa [AbstractCorsController]

Klasa [AbstractCorsController], która będzie klasą nadrzędną dla wszystkich kontrolerów tej aplikacji:

 

Jej kod jest następujący:


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;

    // wysyłanie opcji do klienta
    public void setHeaders(String origin, HttpServletResponse response) {
        // Czy CORS jest dozwolone?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // ustalamy nagłówek CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // zezwalamy na niektóre nagłówki
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // zezwalamy na GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
}
  • wiersz 7: klasa [CorsController] jest klasą abstrakcyjną, ponieważ została zaprojektowana z myślą o rozszerzaniu, a nie o instancjonowaniu;
  • wiersze 13–24: metoda [setHeaders] umieszcza w odpowiedzi [HttpServletResponse response] (wiersz 13) wysyłanej do klienta nagłówki HTTP wymagane przez żądania międzydomenowe;
  • wiersz 33: metoda [/setHeaders] przyjmuje jako parametry:
    • ciąg znaków [origin] występujący w nagłówku HTTP [Origin] żądań międzydomenowych:
Origin:http://localhost:8081

W tym przypadku parametr [origin] w wierszu 13 miałby wartość [http://localhost:8081]. W przypadku, gdy żądanie nie zawiera nagłówka HTTP [Origin], należy zadbać o to, aby uzyskać [origin==null];

  • (ciąg dalszy)
    • obiekt [HttpServletResponse response], który zostanie zwrócony klientowi, który wysłał żądanie;

Te dwa parametry są wstawiane przez Spring;

  • wiersze 15–175: jeśli aplikacja jest skonfigurowana tak, aby akceptować żądania międzydomenowe, a nadawca wysłał nagłówek HTTP [Origin], a źródło to zaczyna się od [http://localhost], wtedy akceptujemy żądanie międzydomenowe, w przeciwnym razie je odrzucamy;
  • wiersz 19: jeśli klient znajduje się w domenie [http://localhost:port], wysyłamy nagłówek HTTP:
Access-Control-Allow-Origin:  http://localhost:port

co oznacza, że serwer akceptuje pochodzenie klienta;

  • wiersz 21: w żądaniu HTTP [OPTIONS] wskazaliśmy dwa konkretne nagłówki HTTP:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

W odpowiedzi na nagłówki HTTP i [Access-Control-Request-X] serwer odpowiada nagłówkami HTTP i [Access-Control-Allow-X], w których określa, co jest dozwolone. Wiersze 20–23 po prostu powtarzają żądanie klienta, aby wskazać, że zostało ono zaakceptowane;

18.9.4. Kontroler [MyControllerWithHttpOptions]

Aby uniknąć konieczności modyfikacji niezabezpieczonego serwera WWW / jSON [intro-server-webjson-01] omówionego w paragrafie 13.5.3, utworzymy nowy kontroler, który w miejscach, gdzie niezabezpieczony serwer przetwarza URL [/url], nowy kontroler będzie przetwarzał żądania o numerach URL i [/cors-url], a ten o numerze URL będzie akceptował żądania międzydomenowe.

Klasa [MyControllerWithHttpOptions] jest kontrolerem, który będzie przetwarzał żądania HTTP typu [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){
        // nagłówki CORS
        setHeaders(origin, httpServletResponse);
    }
...
  • wiersz 14: klasa ta jest kontrolerem Spring o nazwie MVC;
  • wiersz 15: klasa [MyControllerWithHttpOptions] dziedziczy po klasie [AbstractCorsController], którą właśnie opisaliśmy;
  • wiersze 17–18: metoda [getAllCategories] (wiersz 18) przetwarza URL ["/cors-getAllCategories"], gdy jest wywoływana za pomocą metody HTTP [OPTIONS];
  • wiersz 18: metoda [getAllCategories] przyjmuje dwa parametry:
    • [@RequestHeader(value = "Origin", required = false) String origin] w celu pobrania wartości nagłówka HTTP [Origin:http://localhost:8081], o ile występuje. W tym przykładzie parametr [String origin] otrzyma wartość [http://localhost:8081]. Nagłówek ten nie jest obowiązkowy [required = false]. Gdy nie występuje, parametr [String origin] przyjmie wartość null;
    • [HttpServletResponse httpServletResponse]: odpowiedź, która zostanie wysłana do klienta;
  • wiersz 21: wysyłane są nagłówki HTTP, które umożliwiają żądania międzydomenowe. Metoda [setHeaders] jest zdefiniowana w klasie nadrzędnej [AbstractCorsController];

Tak samo postępuje się w przypadku wszystkich metod URL udostępnianych przez serwer WWW / jSON niezabezpieczony [intro-server-webjson-01] omówiony w paragrafie 13.5.3. Gdy ta usługa udostępnia URL oraz [/url], powyższa klasa [MyControllerWithHttpOptions] udostępnia URL oraz [/cors-url].

18.9.5. Kontroler [MyControllerWithCors]

 

Klasa [MyControllerWithCors] to kontroler, który będzie przetwarzał żądania HTTP typu [GET] i [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 {

    // zależności 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 {
        // odpowiedź
        return myController.getAllCategories();
    }
...
  • wiersz 17: klasa [MyControllerWithCors] jest kontrolerem Spring MVC
  • wiersz 18: rozszerza ona klasę [AbstractCorsController];
  • wiersze 21–22: wstrzyknięcie kontrolera [MyController] z serwera WWW / niezabezpieczonego jSON oraz [intro-server-webjson-01] omówionego w paragrafie 13.5.3;
  • wiersze 25–27: metoda [getAllCategories] przetwarza URL [/cors-getAllCategories] (wiersz 28), gdy zostanie wywołana za pomocą metody HTTP [GET];
  • wiersz 26: wynik metody [getAllCategories] zostanie wysłany do klienta. Wynikiem tym jest strumień jSON (atrybut [produces] z wiersza 27 oraz typ [String] wyniku z wiersza 25);
  • wiersz 27: metoda otrzymuje te same parametry, co metoda [getAllCategories] kontrolera [MyControllerWithHttpOptions], którą właśnie omówiliśmy;
  • wiersz 30: metoda [myController.getAllCategories()] otrzymuje polecenie wysłania odpowiedzi;

Ostatecznie to metoda [myController.getAllCategories()] z niezabezpieczonego serwera wysyła odpowiedź. Po prostu wzbogaciliśmy jej odpowiedź o nagłówki niezbędne do żądań międzydomenowych.

Tak samo postępuje się w przypadku wszystkich metod URL udostępnianych przez serwer WWW / jSON – niezabezpieczony serwer [intro-server-webjson-01] omówiony w paragrafie 13.5.3. Gdy ta usługa udostępnia URL oraz [/url], powyższa klasa [MyControllerWithCors] udostępnia URL oraz [/cors-url].

Żądanie międzydomenowe przebiega w następujący sposób:

  • kod JS klienta wysyła żądanie doURL i [/cors-url] wraz z żądaniem HTTP, GET lub POST;
  • przeglądarka wykonująca ten kod przechwytuje to żądanie i najpierw wysyła żądanie o plikURL [/cors-url] z żądaniem HTTP OPTIONS, aby sprawdzić, czy docelowa usługa internetowa akceptuje żądania międzydomenowe;
  • jedna z metod kontrolera [MyControllerWithHttpOptions] wysyła nagłówki międzydomenowe oczekiwane przez przeglądarkę;
  • przeglądarka wysyła następnie początkowe żądanie URL ([/cors-url]) wraz z żądaniem HTTP, GET lub POST;
  • następnie jedna z metod kontrolera [MyControllerWithCors] odpowiada na to żądanie;

18.9.6. Testy

Klasa startowa projektu [intro-spring-cors-server-jpa] wygląda następująco:

  

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);
    }
}
  • wiersz 10: statyczna metoda [SpringApplication.run] jest wykonywana z konfiguracją Spring [AppConfig]. Dzięki tej konfiguracji uruchamiany jest serwer Tomcat wbudowany w archiwa projektu, a na nim wdrażana jest aplikacja internetowa [intro-spring-cors-server-jpa]. Wdrożona jest na nim również aplikacja internetowa z niezabezpieczonego serwera [intro-server-webjson-01], która stanowi część archiwum projektu. Ponieważ projekt [intro-spring-security-server-01] również znajduje się w archiwum, ostatecznie udostępniane są dwa rodzaje adresów URL:
    • adresy bezpiecznej usługi internetowej: /url;
    • adresy serwisu internetowego akceptującego żądania międzydomenowe: /cors-url;

Jesteśmy teraz gotowi do przeprowadzenia nowych testów. Uruchamiamy nową wersję serwisu internetowego i okazuje się, że problem nadal występuje. Nic się nie zmieniło. Jeśli w linii 7 poniżej umieścimy wyświetlanie konsoli, to nigdy nie zostanie ono wyświetlone, co wskazuje, że metoda [getAllCategories] klasy [MyControllerWithHttpOptions] nigdy nie jest wywoływana;


@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) ;
        // nagłówki CORS
        setHeaders(origin, httpServletResponse);
    }

Po przeprowadzeniu kilku badań okazuje się, że domyślnie Spring MVC samodzielnie przetwarza polecenia HTTP i [OPTIONS]. W związku z tym to zawsze Spring odpowiada, a nigdy metoda [getAllCategories] z linii 5 powyżej. To domyślne zachowanie Springa MVC można zmienić. Modyfikujemy istniejącą klasę [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 {

    // żądania międzydomenowe
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @PostConstruct
    public void init() {
        // aplikacja sama przetwarza żądania HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}
  • wiersze 25–26: wstrzyknięcie komponentu [dispatcherServlet], który obsługuje żądania klientów. Komponent ten został zdefiniowany w konfiguracji serwera WWW / jSON (niezabezpieczony) [intro-server-webjson-01] omówionej w paragrafie 13.5.3;
  • wiersze 28–29: metoda [init] (wiersz 29) zostanie wykonana, gdy tylko klasa [AppConfig] zostanie zainicjowana, a wstrzyknięcia Springa zostaną przeprowadzone. Zatem w momencie jej wykonania pole z wiersza 26 zostało już zainicjowane;
  • wiersz 31: konfigurujemy bean [dispatcherServlet] tak, aby pozwolił aplikacji internetowej samodzielnie przetwarzać polecenia HTTP i [OPTIONS];

Ponownie przeprowadzamy testy z tą nową konfiguracją. Otrzymujemy następujący wynik:

  • w [1] widzimy, że istnieją dwa żądania HTTP skierowane do URL i [http://localhost:8080/cors-getAllCategories];
  • w [2] – żądanie [OPTIONS];
  • w [3] znajdują się trzy nagłówki HTTP, które właśnie skonfigurowaliśmy w odpowiedzi serwera;

Przeanalizujmy teraz drugie żądanie:

  • w [1] – analizowane żądanie;
  • w [2], czyli żądanie GET. Dzięki pierwszemu żądaniu [OPTIONS] przeglądarka otrzymała żądane informacje. Teraz wysyła żądanie [GET], o które prosiła początkowo;
  • w [3] – odpowiedź serwera;
  • w [4] serwer wysyła jSON;
  • w pliku [5] wystąpił błąd;
  • w [6] – komunikat o błędzie;

Trudniej jest wyjaśnić, co się tutaj wydarzyło. Odpowiedź serwera [3] jest zgodna z normą [HTTP/1.1 200 OK]. Powinniśmy więc otrzymać żądany dokument. Możliwe, że serwer rzeczywiście wysłał dokument, ale to przeglądarka uniemożliwia jego wykorzystanie, ponieważ wymaga, aby również w przypadku żądania GET odpowiedź zawierała nagłówek HTTP [Access-Control-Allow-Origin:http://localhost:8081].

Modyfikujemy zatem kontroler [MyControllerWithCors], aby również on wysyłał nagłówki niezbędne dla żądań międzydomenowych:


    @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 {
        // nagłówki CORS
        setHeaders(origin, httpServletResponse);
        // odpowiedź
        return myController.getAllCategories();
}
  • wiersz 6: nagłówki niezbędne do żądań międzydomenowych są dołączone do odpowiedzi;

Po tej modyfikacji wyniki są następujące:

Udało nam się uzyskać listę kategorii.

18.10. Pozostałe: URL, [GET]

W kontrolerach [MyControllerWithCors, MyControllerWithHttpOptions], kod akcji przetwarzających żądania URL z [GET] jest zgodny ze wzorcem akcji, które wcześniej przetwarzały URL i [/cors-getAllCategories]. Czytelnik może sprawdzić kod w przykładach dołączonych do niniejszego dokumentu. Oto przykład dla URL i [/cors-getAllProduits]:

w [MyControllerWithHttpOptions]


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

w [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 {
        // nagłówki CORS
        setHeaders(origin, httpServletResponse);
        // odpowiedź
        return myController.getAllProduits();
}

Otrzymany wynik jest następujący:

18.11. URL [POST]

Przyjrzyjmy się następującemu przypadkowi:

  • tworzymy POST [1] w kierunku URL [2];
  • w [3] znajduje się wartość, którą chcemy opublikować. Jest to ciąg znaków jSON;
  • ogólnie rzecz biorąc, chcemy utworzyć kategorię o nazwie [categorie2];

Na razie nie modyfikujemy żadnego kodu. Uzyskany wynik jest następujący:

  • w [1], podobnie jak w przypadku żądań [GET], przeglądarka wysyła żądanie [OPTIONS];
  • w przypadku [2] przeglądarka prosi o zezwolenie na dostęp dla żądania [POST]. Wcześniej było to [GET];
  • w [3] przeglądarka prosi o zezwolenie na wysłanie nagłówków HTTP i [accept, authorization, content-type]. Wcześniej były tylko dwa pierwsze nagłówki;
  • w [4] serwis internetowy nie udziela wszystkich wymaganych uprawnień, co powoduje błąd [5];

Modyfikujemy metodę [AbstractController.sendHeaders] w następujący sposób:


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;

    // wysyłanie opcji do klienta
    public void setHeaders(String origin, HttpServletResponse response) {
        // Czy dopuszczalne są dane Cors?
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // ustawiamy nagłówek CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // zezwalamy na niektóre nagłówki
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // zezwala się na GET i POST
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
    }
}
  • wiersz 21: dodano nagłówek HTTP [Content-Type] (wielkość liter nie ma znaczenia);
  • wiersz 23: dodano metodę HTTP [POST];

Dzięki temu metody [POST] są przetwarzane w taki sam sposób jak zapytania [GET]. Oto przykład dla URL [/cors-addArticles]:

w [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 {
        // nagłówki CORS
        setHeaders(origin, httpServletResponse);
        // odpowiedź
        return myController.addCategories(request);
}

w [MyControllerWithHttpOptions]


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

Otrzymano następujący wynik:

 

Kategoria [categorie2] została pomyślnie dodana do bazy danych. SGBD przypisał jej klucz główny 1729.

18.12. Kontroler [AuthenticateCorsController]

  

Kontroler [AuthenticateCorsController] służy do dostarczaniaURL [/cors-authenticate], który umożliwia wywołanie już istniejącego URL [/authenticate] za pomocą zapytania międzydomenowego. Jego kod jest następujący:


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 {
        // nagłówki CORS
        setHeaders(origin, response);
        // metoda źródłowa
        return authenticateController.authenticate();
    }

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

}

Oto dwa przykłady:

  • wyświetlane odpowiedzi są generowane przez następujący kod: jS:

function doGet(url) {
    // ręczne wywołanie Ajax
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // wynik tekstowy
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // błąd systemowy
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
  • odpowiedź [1] jest wyświetlana przez wiersz 14 funkcji [success];
  • odpowiedź [2] jest wyświetlana przez wiersz 20 funkcji [error]. Funkcja [JSON.stringify] tworzy ciąg znaków jSON z obiektu [jqXHR.statusCode()], który jest obiektem zawierającym wystąpiły błąd. Obiekt ten zawiera niewiele informacji. Można skorzystać z innych metod obiektu [jqXHR], aby uzyskać na przykład nagłówki HTTP zwrócone przez serwer;

18.13. Conclusion

Nasza aplikacja obsługuje teraz żądania międzydomenowe. Można je zezwolić lub zablokować poprzez konfigurację w klasie [AppConfig]:


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

    // żądania międzydomenowe
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @PostConstruct
    public void init() {
        // aplikacja sama przetwarza żądania HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}