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/]:

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.
|
: 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. | ![]() |
|
: przypisuje tekst [blabla] do wszystkich elementów w kolekcji. Powoduje to zmianę treści wyświetlanej na stronie | ![]() |
|
ukrywa elementy kolekcji. Tekst [blabla] nie jest już wyświetlany. | ![]() |
|
: 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. | |
|
: wyświetla elementy kolekcji. Tekst [blabla] pojawia się ponownie. To właśnie atrybut CSS style='display: block;' zapewnia taki wyświetlanie. | ![]() |
|
: ustawia atrybut dla wszystkich elementów kolekcji. Atrybutem tym jest tutaj [style], a jego wartość [color: red]. Tekst [blabla] zmienia kolor na czerwony. | ![]() |
![]() | |
![]() |
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:
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:
co oznacza, że serwer akceptuje pochodzenie klienta;
- wiersz 21: w żądaniu HTTP [OPTIONS] wskazaliśmy dwa konkretne nagłówki HTTP:
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);
}
}







































