18. [Cours]: Управління міждоменним доступом
Ключові слова: CORS (Cross-Origin Resource Sharing).
Цей розділ дещо відхиляється від TD. Його було залишено, оскільки він знайомить із веб-програмуванням та програмуванням на JavaScript. Слід пам’ятати, що однією з цілей цього TD є представлення концепцій, які часто використовуються у розробці JEE, тобто у веб-розробці на основі Java-фреймворків. Тут ми доповнюємо веб-сервер, який використовується в дослідженні бази даних товарів та категорій, щоб він міг приймати міждоменні запити.
У документі [Tutoriel AngularJS / Spring 4] розробляється клієнтсько-серверний додаток, де клієнтом є додаток AngularJS:
![]() |
- сторінки HTML / CSS / JS додатка Angular надходять із сервера [1];
- на сторінці [2] служба [dao] надсилає запит на інший сервер — [2]. Однак це заборонено браузером, який виконує додаток Angular, оскільки це є уразливістю безпеки. Додаток може звертатися лише до того сервера, з якого він походить, тобто до сервера [1];
Насправді невірно стверджувати, що браузер забороняє додатку Angular звертатися до сервера [2]. Насправді додаток звертається до нього, щоб дізнатися, чи дозволяє цей сервер клієнту, який не належить до його домену, звертатися до нього. Цю техніку обміну ресурсами називають CORS (Cross-Origin Resource Sharing). Сервер [2] дає свою згоду, надсилаючи відповідні заголовки HTTP.
Ми створимо таку архітектуру:
![]() |
- у [1] веб-додаток видає сторінки HTML / jS;
- у [2] браузер виконує JavaScript, вбудований у сторінки HTML, щоб звернутися до захищеного веб-сервісу [3];
18.1. Support
![]() |
Проекти цього розділу знаходяться у папці [support / chap-18].
18.2. Проєкт клієнта
Створюємо такий проект Eclipse:
![]() |
18.3. Конфігурація Maven
Проєкт є проєктом Maven із таким файлом: [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>
- рядки 11–15: це проект Spring Boot;
- рядки 23–26: використовується залежність [spring-boot-starter-web], яка містить сервер Tomcat та Spring MVC;
18.4. Конфігурація Spring
![]() |
Клас [WebConfig], який налаштовує проект Spring, має такий вигляд:
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 {
// -------------------------------- конфігурація шару [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/");
}
}
- рядок 15: клас налаштовує проект Spring MVC;
- рядок 16: клас успадковує клас [WebMvcConfigurerAdapter], щоб перевизначити деякі з його методів;
- рядки 18–36: ми вже зустрічали ці біни, наприклад, у розділі 13.5.3.1. Зверніть увагу, що в рядку 35 вказано, що веб-сервіс працюватиме на порту 8081;
- рядки 38–42: метод [addResourceHandlers] дозволяє визначати статичні ресурси, тобто ресурси, які не обробляються методом [DispatcherServlet] із рядка 23;
- рядок 40: на будь-який запит щодо ресурсу з розширенням .html у відповідь буде надано файл, запитуваний у запиті та знайдений у папці [static] у Classpath проекту;
- рядок 41: у відповідь на будь-який запит на ресурс із розширенням .js буде надано файл JavaScript, який запитується у запиті та знайдений у папці [static/js] у Classpath проекту;
![]() |
18.5. Основи jQuery та JavaScript
Сторінка HTML клієнта матиме такий вигляд:
![]() |
Вона міститиме код JavaScript (jS), що виконується у браузері. Ми розглянемо деякі основи JavaScript, які допоможуть нам зрозуміти цей код. Клієнт буде виконувати виклики HTTP за допомогою бібліотеки jQuery [https://jquery.com/], яка надає численні функції, що спрощують розробку на JavaScript. Ми створюємо статичний файл HTML [jQuery.html], який розміщуємо в папці [static]:
![]() |
Цей файл матиме такий вміст:
<!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>
- рядок 6: імпорт файлу jQuery;
- рядки 10–12: елемент сторінки з ідентифікатором [element1]. Ми попрацюємо з цим елементом.
Нам потрібно завантажити файл [jquery-2.1.3.min.js]. Останню версію jQuery можна знайти за посиланням URL [http://jquery.com/download/]:

Завантажений файл слід помістити в папку [static / js] і змінити рядок 6 у файлі HTML відповідно до встановленої версії.
Після цього відкриваємо статичний вигляд [jQuery.html] у браузері Chrome [1-2]:
![]() |
У Google Chrome виконайте [Ctrl-Maj-I], щоб відкрити інструменти розробки [3]. Вкладка [Console] [4] дозволяє виконувати код JavaScript. Нижче наведено команди JavaScript, які слід ввести, та їх пояснення.
|
: створює колекцію всіх елементів з ідентифікатором [element1], тобто, як правило, колекцію з 0 або 1 елемента , оскільки на сторінці HTML не може бути двох однакових ідентифікаторів. | ![]() |
|
: застосовує текст [blabla] до всіх елементів колекції. Це призводить до зміни вміст, що відображається на сторінці | ![]() |
|
приховує елементи колекції. Текст [blabla] більше не відображається. | ![]() |
|
: знову відображає колекцію. Це дозволяє побачити, що елемент з ідентифікатором [element1] має атрибут CSS style='display: none;', який приховує цей елемент. | |
|
: відображає елементи колекції. Текст [blabla] знову з’являється. Це атрибут CSS style='display: block;', який забезпечує таке відображення. | ![]() |
|
: встановлює атрибут для всіх елементів колекції. Атрибут тут — [style], а його значення [color: red]. Текст [blabla] стає червоним. | ![]() |
![]() | |
![]() |
Слід зауважити, що значення URL у браузері не змінилося під час усіх цих операцій. Обміну даними з веб-сервером не відбувалося. Усе відбувається всередині браузера. Тепер переглянемо вихідний код сторінки:
![]() | ![]() |
Це початковий текст. Він жодним чином не відображає маніпуляції, які ми виконали з елементом у рядках 10–12. Про це важливо пам’ятати під час налагодження JavaScript. Тому часто немає сенсу переглядати вихідний код відображеної сторінки.
18.6. JavaScript-код додатка
Повернемося до сторінки клієнтського додатка, яка буде звертатися до веб-сервісу / jSON:
![]() |
![]() |
Код HTML цієї сторінки такий:
<!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">
<!-- ідентифікатор -->
Identifiant :
<!-- -->
<input type="text" id="identifiant" name="identifiant" value="" />
<!-- пароль -->
<br /> <br /> Mot de passe :
<!-- -->
<input type="text" id="password" name="password" value="" />
<!-- метод 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 />
<!-- значення, введене -->
<br /> Chaîne jSON à poster : <input type="text" id="posted"
size="50" />
<!-- кнопка підтвердження -->
<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>
- рядок 6: імпортуємо бібліотеку jQuery;
- рядок 7: імпортується код, який ми будемо писати;
- рядки 15, 19, 26, 29, 31: запишемо ідентифікатори [id] компонентів сторінки. JavaScript посилається на ці компоненти через ці ідентифікатори;
Код [client.js] має такий вигляд:
// загальні дані
var url;
var posted;
var response;
var method;
var baseUrl = 'http://localhost:8080';
var identifiant;
var password;
var authorizationHeader;
function requestServer() {
// отримуємо інформацію
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);
// очищаємо попередню відповідь
response.text("");
// виконується ручний виклик Ajax
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// виконуємо виклик Ajax вручну
$.ajax({
headers : {
'Authorization':'Basic '+authorizationCode
},
url : baseUrl + url,
type : 'GET',
dataType : 'text',
beforeSend : function() {
},
success : function(data) {
// текстовий результат
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// системна помилка
response.text(JSON.stringify(jqXHR.statusCode()));
}
})
}
function doPost(url, posted) {
// виконується ручний виклик Ajax
$.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) {
// результат у вигляді тексту
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// системна помилка
response.text(JSON.stringify(jqXHR.statusCode()));
}
})
}
// під час завантаження документа
$(document).ready(function() {
// отримуємо посилання на компоненти сторінки
identifiant = $("#identifiant");
password = $("#password");
url = $("#url");
posted = $("#posted");
response = $("#response");
});
- рядки 80–87: код jS, що виконується після завершення завантаження документа в браузері;
- рядки 81–86: отримуються посилання на різні елементи документа HTML за допомогою їхніх ідентифікаторів [id];
- рядки 2–9: глобальні змінні, доступні у всіх функціях, визначених у файлі jS;
- рядок 13: отримуємо значення URL, введене користувачем;
- рядок 14: отримуємо значення, яке він хоче опублікувати (порожнє, якщо операція GET);
- рядок 15: отримуємо ідентифікатор, введений користувачем;
- рядок 16: отримуємо його пароль;
- рядок 17: отримуємо спосіб [get] або [post], який слід використовувати для запиту URL з рядка 9:
- [document] позначає документ, завантажений браузером, що називається DOM (Document Object Model),
- [document.forms[0]] позначає першу форму в документі; документ може містити кілька форм. У даному випадку є лише одна,
- [document.forms[0].elements['method']] позначає елемент форми, який має атрибут [name='method']. Їх є два:
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
- (продовження)
- [document.forms[0].elements['method'].value] — це значення, яке буде відправлено для компонента, що має атрибут [name='method']. Відомо, що відправлене значення — це значення атрибута [value] радіо-кнопки, яка позначена. Отже, у цьому випадку це буде один із рядків ['get', 'post'];
- рядок 18: формується Base74-кодування рядка «identifiant:password». Цей закодований рядок буде використано в заголовку HTTP [Authorization], який ми надішлемо на сервер для автентифікації запиту;
- рядки 22–26: залежно від того, який метод HTTP потрібно використовувати, виконується метод [doGet] або [doPost];
- метод jQuery або [$.ajax] виконує виклик HTTP;
- рядки 32–34: здійснюється звернення до сервера, який вимагає заголовок HTTP або [Authorization: Basic code];
- рядок 35: користувач введе URL типу [/cors-getAllCategories,/cors-addProduits, ...]. Тому ці URL потрібно доповнити URL сервера з рядка 6;
- рядок 36: слід використовувати метод HTTP;
- рядок 37: сервер повертає jSON. Вказується тип [text] як тип результату, щоб відобразити його таким, яким він був отриманий;
- рядок 42: відображення текстової відповіді сервера;
- рядки 48–49: виведення можливого повідомлення про помилку;
- рядок 53: метод [doPost] отримує другий параметр, який є значенням для відправлення;
- рядок 61: для вказівки, що значення буде відправлено у вигляді рядка jSON;
18.7. Виконання клієнтської програми
Клієнтський додаток — це додаток Spring Boot, що запускається наступним виконуваним класом [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);
}
}
- рядок 10: метод [SpringApplication.run] використовує файл конфігурації [WebConfig]. Сторінка [client.html] буде розгорнута на сервері Tomcat, який знаходиться в Classpath проекту;
18.8. URL [/getAllCategories]
Ми запускаємо:
- веб-сервер / json на порту 8080;
- клієнт цього сервера на порту 8081;
потім запитуємо URL [http://localhost:8081/client.html] [1]:
![]() |
- у [2], виконуємо GET на URL [http://localhost:8080/getAllCategories];
Ми не отримуємо відповіді від сервера. При перегляді консолі розробника Chrome (Ctrl-Shift-I) виявляється помилка:
![]() |
- у [1], ми перебуваємо у вкладці [Network];
- у [2] бачимо, що запит HTTP, який було зроблено, — це не [GET], а [OPTIONS]. У разі міждоменного запиту браузер перевіряє на сервері, чи виконано певну кількість умов, надсилаючи йому запит HTTP [OPTIONS]. У даному випадку запити позначені маркерами [5-6];
- у запиті [5] браузер запитує, чи можна дістатися до адресата URL за допомогою GET. Заголовок запиту [Access-Control-Request-Method] вимагає відповіді із заголовком HTTP [Access-Control-Allow-Methods], що вказує на те, що запитуваний метод прийнятий;
- у [6] браузер надсилає заголовок HTTP [Origin: http://localhost:8081]. Цей заголовок вимагає відповіді у вигляді заголовка HTTP [Access-Control-Allow-Origin], що вказує на те, що вказане джерело прийнято;
- у заголовку [7] браузер запитує, чи прийнятні заголовки HTTP, [accept] та [authorization]. Заголовок запиту [Access-Control-Request-Headers] очікує відповіді із заголовками HTTP та [Access-Control-Allow-Headers], що вказують на те, що запитувані заголовки прийнято;
- у [3] виникає помилка. При натисканні на іконку з’являється помилка [4];
- у [4] повідомлення вказує, що сервер не надіслав заголовок HTTP [Access-Control-Allow-Origin], який вказує, чи прийнято джерело запиту;
- у [8] можна побачити, що сервер дійсно не надіслав цей заголовок. Як наслідок, браузер відмовився виконати запит HTTP GET, який було подано спочатку;
Нам потрібно змінити налаштування веб-сервера / jSON.
18.9. Нова веб-служба / json
Створюємо новий проект Maven [intro-spring-cors-server-jpa]:
![]() | ![]() |
18.9.1. Конфігурація Maven
Конфігурація Maven для нового веб-сервісу виглядає так:
<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>
<!-- плагіни -->
<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>
- рядки 23–27: ми використовуємо всі результати роботи, виконаної до цього моменту, спираючись на архів захищеного веб-сервера / json;
18.9.2. Конфігурація Spring
Клас конфігурації [AppConfig] має такий вигляд:
![]() |
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 {
// міждоменні запити
@Bean
public boolean isCorsEnabled() {
return true;
}
}
- рядок 10: клас є класом конфігурації Spring;
- рядок 11: інші компоненти Spring слід шукати в пакеті [spring.cors.server.service];
- рядки 16–19: ми створюємо компонент Spring із назвою [isCorsEnabled], який визначає, чи приймаються клієнти, що не належать до домену сервера;
18.9.3. Клас [AbstractCorsController]
Клас [AbstractCorsController], який буде батьківським класом для всіх контролерів цього додатка:
![]() |
Його код такий:
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;
// надсилання параметрів клієнту
public void setHeaders(String origin, HttpServletResponse response) {
// CORS дозволено?
if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
return;
}
// встановлюємо заголовок CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// дозволено певні заголовки
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// дозволяється GET
response.addHeader("Access-Control-Allow-Methods", "GET");
}
}
- рядок 7: клас [CorsController] є абстрактним, оскільки призначений для розширення, а не для створення екземплярів;
- рядки 13–24: метод [setHeaders] вставляє у відповідь [HttpServletResponse response] (рядок 13), що надсилається клієнту, заголовки HTTP, які вимагаються міждоменними запитами;
- рядок 33: метод [/setHeaders] приймає як параметри:
- рядок [origin], що міститься в заголовку HTTP [Origin] міждоменних запитів:
У цьому випадку параметр [origin] у рядку 13 матиме значення [http://localhost:8081]. У разі, якщо запит не містить заголовка HTTP [Origin], ми подбаємо про те, щоб отримати [origin==null];
- (продовження)
- об’єкт [HttpServletResponse response], який буде повернуто клієнту, що зробив запит;
Ці два параметри вставляються Spring;
- рядки 15–175: якщо додаток налаштовано на прийом міждоменних запитів, і якщо відправник надіслав заголовок HTTP [Origin], і якщо це джерело починається з [http://localhost], то міждоменний запит буде прийнято, інакше — відхилено;
- рядок 19: якщо клієнт знаходиться в домені [http://localhost:port], надсилається заголовок HTTP:
що означає, що сервер приймає домен клієнта;
- рядок 21: ми вказали два особливі заголовки HTTP у запиті HTTP [OPTIONS]:
На заголовок HTTP [Access-Control-Request-X] сервер відповідає заголовком HTTP [Access-Control-Allow-X], у якому вказує, що дозволено. У рядках 20–23 просто повторюється запит клієнта, щоб вказати, що він прийнятий;
18.9.4. Контролер [MyControllerWithHttpOptions]
Щоб не доводилося модифікувати незахищений веб-сервер / jSON [intro-server-webjson-01], розглянутий у розділі 13.5.3, ми створимо новий контролер, який замість незахищеного сервера оброблятиме запити URL та [/url], а новий контролер — запити URL та [/cors-url], причому URL прийматиме міждоменні запити.
Клас [MyControllerWithHttpOptions] — це контролер, який оброблятиме запити HTTP типу [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){
// заголовки CORS
setHeaders(origin, httpServletResponse);
}
...
- рядок 14: клас є контролером Spring MVC;
- рядок 15: клас [MyControllerWithHttpOptions] успадковує клас [AbstractCorsController], який ми щойно описали;
- рядки 17–18: метод [getAllCategories] (рядок 18) обробляє URL ["/cors-getAllCategories"], коли її викликають за допомогою методу HTTP [OPTIONS];
- рядок 18: метод [getAllCategories] приймає два параметри:
- [@RequestHeader(value = "Origin", required = false) String origin] для отримання значення заголовка HTTP [Origin:http://localhost:8081], якщо він присутній. У цьому прикладі параметр [String origin] отримає значення [http://localhost:8081]. Цей заголовок не є обов’язковим [required = false]. Якщо він відсутній, параметр [String origin] матиме значення null;
- [HttpServletResponse httpServletResponse]: відповідь, яка буде надіслана клієнту;
- рядок 21: надсилаються заголовки HTTP, які дозволяють міждоменні запити. Метод [setHeaders] визначено в батьківському класі [AbstractCorsController];
Так робиться для всіх URL, що надаються веб-сервером / jSON незахищеним [intro-server-webjson-01], розглянутим у розділі 13.5.3. Коли ця служба надає доступ до URL та [/url], вищезазначений клас [MyControllerWithHttpOptions] надає доступ до URL та [/cors-url].
18.9.5. Контролер [MyControllerWithCors]
![]() |
Клас [MyControllerWithCors] — це контролер, який оброблятиме запити HTTP типів [GET] та [POST]:
package spring.cors.server.service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import spring.webjson.service.MyController;
@Controller
public class MyControllerWithCors extends AbstractCorsController {
// залежності Spring
@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 {
// відповідь
return myController.getAllCategories();
}
...
- рядок 17: клас [MyControllerWithCors] є контролером Spring MVC
- рядок 18: він успадковує клас [AbstractCorsController];
- рядки 21–22: ін’єкція контролера [MyController] із веб-сервера / незахищеного jSON [intro-server-webjson-01], розглянутого в розділі 13.5.3;
- рядки 25–27: метод [getAllCategories] обробляє URL [/cors-getAllCategories] (рядок 28), коли це запитується за допомогою методу HTTP [GET];
- рядок 26: результат методу [getAllCategories] буде надіслано клієнту. Цей результат є потоком jSON (атрибут [produces] у рядку 27 та тип [String] результату у рядку 25);
- рядок 27: метод отримує ті самі параметри, що й метод [getAllCategories] контролера [MyControllerWithHttpOptions], який ми щойно розглянули;
- рядок 30: метод [myController.getAllCategories()] отримує запит на відправлення відповіді;
У підсумку відповідь надсилає метод [myController.getAllCategories()] незахищеного сервера. Ми просто доповнили його відповідь заголовками, необхідними для міждоменних запитів.
Так робиться для всіх методів URL, що надаються веб-сервером / jSON незахищеним [intro-server-webjson-01], розглянутим у розділі 13.5.3. Коли цей сервіс надає URL та [/url], вищезазначений клас [MyControllerWithCors] надає URL та [/cors-url].
Міждоменний запит відбуватиметься наступним чином:
- код JS клієнта надсилає запит доURL та [/cors-url] із запитом HTTP, GET або POST;
- браузер, що виконує цей код, перехоплює цей запит і спочатку надсилає запит наURL [/cors-url] із запитом HTTP OPTIONS, щоб перевірити, чи цільовий веб-сервіс приймає міждоменні запити;
- один із методів контролера [MyControllerWithHttpOptions] надсилає міждоменні заголовки, яких очікує браузер;
- потім браузер надсилає початковий запит URL ([/cors-url]) із запитом HTTP, GET або POST;
- після чого один із методів контролера [MyControllerWithCors] надає відповідь;
18.9.6. Тестування
Клас завантаження проекту [intro-spring-cors-server-jpa] має такий вигляд:
![]() |
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);
}
}
- рядок 10: статичний метод [SpringApplication.run] виконується з конфігурацією Spring [AppConfig]. Завдяки цій конфігурації запускається вбудований у архіви проєкту сервер Tomcat, і на ньому розгортається веб-додаток [intro-spring-cors-server-jpa]. Веб-додаток незахищеного сервера [intro-server-webjson-01], що входить до складу архіву проєкту, також розгортається на ньому. Оскільки проєкт [intro-spring-security-server-01] також входить до складу архіву, у підсумку відкриваються два типи URL:
- адреси захищеного веб-сервісу: /url;
- адреси веб-сервісу, що приймає міждоменні запити: /cors-url;
Тепер ми готові до нових тестів. Ми запускаємо нову версію веб-сервісу і виявляємо, що проблема залишається невирішеною. Нічого не змінилося. Якщо в 7-му рядку нижче розмістити вивід у консолі, він ніколи не відображається, що свідчить про те, що метод [getAllCategories] класу [MyControllerWithHttpOptions] ніколи не викликається;
@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) ;
// заголовки CORS
setHeaders(origin, httpServletResponse);
}
Після невеликого дослідження виявляється, що за замовчуванням Spring MVC самостійно обробляє команди HTTP та [OPTIONS]. Тому відповідь завжди надає Spring, а не метод [getAllCategories] із рядка 5 вище. Цю поведінку Spring MVC за замовчуванням можна змінити. Ми модифікуємо існуючий клас [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 {
// міждоменні запити
@Bean
public boolean isCorsEnabled() {
return true;
}
@Autowired
private DispatcherServlet dispatcherServlet;
@PostConstruct
public void init() {
// додаток самостійно обробляє запити HTTP [OPTIONS]
dispatcherServlet.setDispatchOptionsRequest(true);
}
}
- рядки 25–26: ін’єкція біна [dispatcherServlet], який обробляє запити клієнтів. Цей бін було визначено в конфігурації незахищеного веб-сервера / jSON, розглянутого в розділі 13.5.3;
- рядки 28–29: метод [init] (рядок 29) буде виконано, щойно клас [AppConfig] буде інстанційовано та виконано ін’єкції Spring. Отже, на момент його виконання поле з рядка 26 вже було ініціалізовано;
- рядок 31: налаштовуємо бін [dispatcherServlet] так, щоб він дозволяв веб-додатку самостійно обробляти команди HTTP та [OPTIONS];
Ми повторюємо тести з цією новою конфігурацією. Отримуємо такий результат:
![]() |
- у [1] ми бачимо, що є два запити HTTP до URL [http://localhost:8080/cors-getAllCategories];
- у [2] — запит [OPTIONS];
- у [3] — три заголовки HTTP, які ми щойно налаштували у відповіді сервера;
Тепер розглянемо другий запит:
![]() |
- у [1] — це розглянутий запит;
- у [2] — це запит GET. Завдяки першому запиту [OPTIONS] браузер отримав інформацію, яку він запитував. Тепер він виконує запит [GET], який був запрошений спочатку;
- у [3] — відповідь сервера;
- у [4] сервер надсилає jSON;
- у файлі [5] — сталася помилка;
- у [6] — повідомлення про помилку;
Тут складніше пояснити, що саме сталося. Відповідь сервера [3] є нормальною — [HTTP/1.1 200 OK]. Отже, ми мали б отримати запитуваний документ. Можливо, сервер дійсно надіслав документ, але браузер заважає його використанню, оскільки вимагає, щоб у відповіді на запит GET також містився заголовок HTTP [Access-Control-Allow-Origin:http://localhost:8081].
Тож ми змінюємо контролер [MyControllerWithCors], щоб він також надсилав необхідні заголовки для міждоменних запитів:
@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 {
// заголовки CORS
setHeaders(origin, httpServletResponse);
// відповідь
return myController.getAllCategories();
}
- рядок 6: заголовки, необхідні для міждоменних запитів, включені у відповідь;
Після цієї зміни результати такі:
![]() |
Ми дійсно отримали список категорій.
18.10. Інші URL [GET]
У контролерах [MyControllerWithCors, MyControllerWithHttpOptions] код дій, що обробляють запити URL із [GET], відповідає шаблону дій, які раніше обробляли URL та [/cors-getAllCategories]. Читач може перевірити код у прикладах, що додаються до цього документа. Ось приклад для URL та [/cors-getAllProduits]:
у [MyControllerWithHttpOptions]
@RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.OPTIONS)
public void getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
HttpServletResponse httpServletResponse) {
// заголовки CORS
setHeaders(origin, httpServletResponse);
}
у [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 {
// заголовки CORS
setHeaders(origin, httpServletResponse);
// відповідь
return myController.getAllProduits();
}
Отримано такий результат:
![]() |
18.11. URL [POST]
Розглянемо такий випадок:
![]() |
- здійснюється перехід від POST [1] до URL [2];
- у [3] — значення, що передається. Це рядок jSON;
- загалом ми прагнемо створити категорію під назвою [categorie2];
Наразі ми не змінюємо жодного коду. Отриманий результат такий:
![]() |
- у [1], як і для запитів [GET], браузер надсилає запит [OPTIONS];
- у випадку [2] браузер запитує дозвіл на доступ для запиту [POST]. Раніше це був [GET];
- у [3] він запитує дозвіл на відправку заголовків HTTP та [accept, authorization, content-type]. Раніше були лише перші два заголовки;
- у [4] веб-сервіс не надає всіх запитуваних дозволів, що спричиняє помилку [5];
Ми змінюємо метод [AbstractController.sendHeaders] наступним чином:
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;
// надсилання параметрів клієнту
public void setHeaders(String origin, HttpServletResponse response) {
// Cors дозволено?
if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
return;
}
// встановлюємо заголовок CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// дозволено певні заголовки
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// дозволено GET та POST
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
}
- рядок 21: додано заголовок HTTP [Content-Type] (регістр не має значення);
- рядок 23: додано метод HTTP [POST];
Таким чином, методи [POST] обробляються так само, як і запити [GET]. Ось приклад URL [/cors-addArticles]:
у [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 {
// заголовки CORS
setHeaders(origin, httpServletResponse);
// відповідь
return myController.addCategories(request);
}
у [MyControllerWithHttpOptions]
@RequestMapping(value = "/cors-addCategories", method = RequestMethod.OPTIONS)
public void addCategories(HttpServletRequest request,
@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse httpServletResponse)
throws JsonProcessingException {
// заголовки CORS
setHeaders(origin, httpServletResponse);
}
Отримано такий результат:
![]() |
Категорія [categorie2] була успішно додана до бази даних. SGBD присвоїв їй первинний ключ 1729.
18.12. Контролер [AuthenticateCorsController]
![]() |
Контролер [AuthenticateCorsController] призначений для наданняURL [/cors-authenticate], що дозволяє викликати вже існуючі URL [/authenticate] за допомогою міждоменного запиту. Його код такий:
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 {
// заголовки CORS
setHeaders(origin, response);
// метод походження
return authenticateController.authenticate();
}
@RequestMapping(value = "/cors-authenticate", method = RequestMethod.OPTIONS)
public void corsAuthenticate(@RequestHeader(value = "Origin", required = false) String origin,
HttpServletResponse response) {
// заголовки CORS
setHeaders(origin, response);
}
}
Ось два приклади:
![]() |
- відповіді відображаються за допомогою такого коду: jS:
function doGet(url) {
// Ajax-запит виконується вручну
$.ajax({
headers : {
'Authorization':'Basic '+authorizationCode
},
url : baseUrl + url,
type : 'GET',
dataType : 'text',
beforeSend : function() {
},
success : function(data) {
// текстовий результат
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// системна помилка
response.text(JSON.stringify(jqXHR.statusCode()));
}
})
}
- відповідь [1] відображається 14-м рядком функції [success];
- відповідь [2] відображається рядком 20 функції [error]. Функція [JSON.stringify] створює рядок jSON об’єкта [jqXHR.statusCode()], який є об’єктом, що інкапсулює помилку, яка сталася. Цей об’єкт надає мало інформації. Можна використовувати інші методи об’єкта [jqXHR], щоб отримати, наприклад, заголовки HTTP, повернуті сервером;
18.13. Conclusion
Наш додаток тепер підтримує міждоменні запити. Їх можна дозволити або заборонити за допомогою налаштувань у класі [AppConfig]:
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ SecurityConfig.class })
public class AppConfig {
// міждоменні запити
@Bean
public boolean isCorsEnabled() {
return true;
}
@Autowired
private DispatcherServlet dispatcherServlet;
@PostConstruct
public void init() {
// додаток самостійно обробляє запити HTTP [OPTIONS]
dispatcherServlet.setDispatchOptionsRequest(true);
}
}







































