Skip to content

21. Управління міждоменним доступом

21.1. Architecture

Тепер розглянемо проблему міждоменних запитів. У документі [Tutoriel AngularJS / Spring 4] розробляється клієнт-серверна програма, де клієнтом є програма AngularJS:

  • сторінки HTML / CSS / JS додатка Angular надходять із сервера [1];
  • на сторінці [2] служба [dao] надсилає запит на інший сервер — [2]. Але це заборонено браузером, який виконує додаток Angular, оскільки це є уразливістю безпеки. Додаток може звертатися лише до того сервера, з якого він походить, тобто до сервера [1];

Насправді не зовсім правильно стверджувати, що браузер забороняє додатку Angular звертатися до сервера [2]. Насправді браузер звертається до сервера [2], щоб дізнатися, чи дозволяє той клієнту, який не походить з його домену, звертатися до нього. Цю техніку обміну називають CORS (Cross-Origin Resource Sharing). Сервер [2] дає свою згоду, надсилаючи певні заголовки HTTP.

Щоб продемонструвати проблеми, з якими можна зіткнутися, ми створимо клієнтсько-серверний додаток, у якому:

  • сервером буде наш захищений веб-сервер / jSON;
  • клієнтом буде проста сторінка HTML, оснащена кодом JavaScript, який надсилатиме запити до веб-сервера / jSON;

Ми реалізуємо таку архітектуру:

  • на [1] веб-додаток видає сторінки HTML / jS;
  • у [2] браузер виконує вбудований у сторінки HTML JavaScript для звернення до захищеного веб-сервісу [3];

21.2. Проєкт [spring-cors-server-jdbc-generic]

21.2.1. Налаштування робочого середовища

  
  • завантажте вищезазначені проекти. Проекти [spring-cors-*] знаходяться у папці [<exemples>\spring-database-generic\spring-cors];
  • натисніть Alt-F5 і перегенеруйте всі проекти Maven;

Потім запустіть конфігурацію виконання з назвою [spring-cors-server-jdbc-generic] (повинні запуститися SGBD та MySQL), яка запускає веб-сервіс на порту 8081:

 

Заповніть базу даних [dbproduitscategories] за допомогою конфігурації виконання з назвою [spring-jdbc-generic-04-fillDataBase]:

 

Виконайте конфігурацію виконання з назвою [spring-cors-client-generic], яка запускає другий веб-додаток (на іншому Tomcat) на порту 8082:

 

У браузері завантажте URL [http://localhost:8082/client.html]:

  • у [1] запитується коротка версія всіх категорій;
  • у [2] — відповідь jSON від сервера;

21.2.2. Проект клієнта [spring-cors-client-generic]

  

Файл [application.properties] дозволяє нам встановити порт веб-додатку клієнта. Його вміст такий:


server.port=8082

Отже:

  • клієнт — це веб-додаток, доступний за адресою URL [http://localhost:8082];
  • сервер — це веб-додаток, доступний за адресами URL та [http://localhost:8081];

Оскільки клієнт не отримує дані з того самого порту, що й сервер, виникає проблема міждоменних запитів. Адже [http://localhost:8081] та [http://localhost:8082] — це два різні домени.

21.2.3. Налаштування Maven

Проєкт є проєктом Maven із таким файлом [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<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>dvp.spring.database</groupId>
    <artifactId>spring-cors-client-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-cors-client-generic</name>
    <description>Client cors for webjson server</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- пошук батьківського елемента в репозиторії -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <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>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • рядки 14–19: це проєкт Spring Boot;
  • рядки 27–30: використовується залежність [spring-boot-starter-web], яка включає в себе сервер Tomcat та Spring MVC;

21.2.4. Основи jQuery та JavaScript

Веб-додаток відображає таку єдину сторінку:

 

Вона містить код 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="/js/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/]:

Image

Завантажений файл слід помістити в папку [static / js]:

  

Після цього відкрийте статичний вигляд [jQuery.html] у браузері Chrome [1-2]:

У Google Chrome виконайте [Ctrl-Maj-I], щоб відобразити інструменти розробки [3]. Вкладка [Console] [4] дозволяє виконувати код JavaScript. Нижче наведено команди JavaScript, які слід ввести, та їх пояснення.

JS
résultat
$("#element1")
: створює колекцію всіх елементів з ідентифікатором
[element1], тобто, як правило, колекцію
з 0 або 1 елементом, оскільки на одній сторінці не може бути
двох однакових ідентифікаторів на одній сторінці HTML.
$("#element1").text("blabla")
: застосовує текст [blabla] до всіх елементів
колекції. Це призводить до зміни
вміст, що відображається на сторінці
$("#element1").hide()
приховує елементи колекції. Текст
[blabla] більше не відображається.
$("#element1")
: знову відображає колекцію. Це
дозволяє побачити, що елемент з ідентифікатором [element1] має
атрибут CSS style='display: none;', що
приховує цей елемент.
$("#element1").show()
: відображає елементи колекції. Текст
[blabla] знову з’являється. Це атрибут
CSS style='display: block;', який забезпечує таке
відображення.
$("#element1").attr('style','color: red')
: встановлює атрибут для всіх елементів
колекції. Атрибут тут — [style], а його значення
[color: red]. Текст [blabla] стає червоним.
Tableau
Dictionnaire

Слід зазначити, що код URL браузера не змінився під час усіх цих операцій. Обміну даними з веб-сервером не відбувалося. Усе відбувається всередині браузера. А тепер переглянемо вихідний код сторінки:


<!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="/js/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>

Це вихідний текст. Він жодним чином не відображає маніпуляції, які ми провели з елементом у рядках 10–12. Про це важливо пам’ятати під час налагодження JavaScript. Тому часто немає сенсу переглядати вихідний код відображеної сторінки.

21.2.5. Код jS додатка

Повернемося до коду HTML на сторінці клієнтського додатка, який буде звертатися до веб-сервісу /jSON:

 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
    <h2>Client du service web / jSON</h2>
    <form id="formulaire">
        <!--  метод HTTP -->
        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 : <input type="text" id="url" size="30"><br />
        <!-- значення, що надсилається -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
        <!-- кнопка підтвердження -->
        <br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
    </form>
    <hr />
    <h2>Réponse du serveur</h2>
    <div id="response"></div>
</body>
</html>
  • рядок 6: імпортуємо бібліотеку jQuery;
  • рядок 7: імпортуємо код, який ми будемо писати;
  • рядки 11, 15, 17, 21: запишемо ідентифікатори [id] компонентів сторінки. JavaScript посилається на ці компоненти через ці ідентифікатори;

Код [client.js] має такий вигляд:


// загальні дані
var url;
var posted;
var response;
var method;

function requestServer() {
    // отримуємо інформацію з форми
    var urlValue = url.val();
    var postedValue = posted.val();
    method = document.forms[0].elements['method'].value;
    // виконується ручний виклик Ajax
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // виконується ручний виклик Ajax
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8081' + url,
        type : 'GET',
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // текстовий результат
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // системна помилка
            response.text(jqXHR.responseText);
        }
    })
}

function doPost(url, posted) {
    // виконується ручний виклик Ajax
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8081    ' + url,
        type : 'POST',
        contentType : 'application/json',
        data : posted,
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // текстовий результат
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // системна помилка
            response.text(jqXHR.responseText);
        }
    })
}

// під час завантаження документа
$(document).ready(function() {
    // отримано посилання на компоненти сторінки
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • рядки 71–75: код jS, що виконується після завершення завантаження документа в браузері;
  • рядки 73–75: отримуються посилання на три області документа HTML;
  • рядки 2–5: глобальні змінні, відомі у всіх функціях, визначених у файлі jS;
  • рядок 9: отримується значення URL, введене користувачем;
  • рядок 10: отримується значення, яке він хоче відправити;
  • рядок 11: отримуємо спосіб [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'];
  • рядки 13–18: залежно від того, який метод HTTP потрібно використовувати, виконується метод [doGet] або [doPost];
  • метод jQuery [$.ajax] виконує виклик HTTP;
  • рядки 23–25: здійснюється звернення до сервера, який вимагає заголовок HTTP або [Authorization: Basic code]. Ми створюємо цей заголовок для користувача [admin / admin], який є єдиним, хто може надсилати запити до сервера;
  • рядок 26: користувач введе URL типу [/getAllLongCategories, /saveCategories, ...]. Тому потрібно заповнити ці URL;
  • рядок 27: слід використовувати метод HTTP;
  • рядок 28: сервер повертає jSON. Вказуємо тип [text/plain] як тип результату, щоб відобразити його таким, яким він був отриманий;
  • рядок 33: відображення текстової відповіді сервера;
  • рядок 39: виведення можливого повідомлення про помилку у текстовому форматі;
  • рядок 44: метод [doPost] отримує другий параметр, який є значенням для відправлення;
  • рядок 52: для вказівки, що значення, яке надсилається, буде у формі рядка jSON;

21.2.6. Виконання клієнтської програми

Клієнтський додаток — це консольний додаток, що запускається за допомогою такого виконуваного класу [Client]:

  

package spring.cors.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

@EnableAutoConfiguration
public class Client {

    public static void main(String[] args) {
        SpringApplication.run(Client.class, args);
    }
}
  • рядок 6: анотація [@EnableAutoConfiguration] є анотацією проекту [Spring Boot] (рядок 4). Spring Boot перевірить архіви, що містяться у Classpath проекту. У даному випадку це будуть усі залежності Maven, що входять до єдиної залежності файлу [pom.xml]:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
</dependencies>

Ця залежність приносить дуже багато архівів, зокрема Spring MVC та сервер Tomcat. Через наявність цих залежностей Spring Boot налаштує, використовуючи значення за замовчуванням, проект Spring MVC, що виконується на Tomcat. Сервер Tomcat налаштовується на роботу на порту 8080. Якщо ви хочете відійти від значень за замовчуванням, обраних Spring Boot, можна використати файл [application.properties] у кореневому каталозі Classpath (усе, що міститься в [src / main / resources], знаходиться в кореневому каталозі Classpath):

  

Ми вказуємо, що сервер Tomcat повинен працювати на порту 8082 наступним чином:


server.port=8082

Перелік параметрів, які можна використовувати в [application.properties], наведено в URL (червень 2015 р.) [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html];

Повернемося до коду [Client.java]:

  • рядок 10: метод [SpringApplication.run] розгорне сторінку [client.html] на сервері Tomcat, який знаходиться у Classpath проекту;

21.2.7. URL [/getAllShortCategories]

Ми запускаємо:

  • захищений веб-сервер / json на порту 8081 (конфігурація [spring-security-server-jdbc-generic]);
  • клієнт цього сервера на порту 8082 (конфігурація [spring-cors-client-generic]);

потім запитуємо URL [http://localhost:8082/client.html] [1]:

  • у [2] ми виконуємо GET над URL та [http://localhost:8081/getAllShortCategories];

Ми не отримуємо відповіді від сервера. При перегляді консолі розробника 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.

21.2.8. Нова веб-служба / json

Створюємо новий проект Maven [spring-cors-server-jdbc-generic]:

 

Конфігурація 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>dvp.spring.database</groupId>
    <artifactId>spring-cors-server-jdbc-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-cors-server-jdbc-generic</name>
    <description>démo spring cors</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <!-- плагіни -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jdbc-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
  • рядки 30–32: ми використовуємо всі результати роботи, виконаної до цього моменту, спираючись на архів захищеного веб-сервера / json;

У підсумку залежності виглядають так:

  

Конфігураційний клас [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;

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

    // міждоменні запити
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
...
}
  • рядок 12: клас є класом конфігурації Spring;
  • рядок 9: інші компоненти Spring слід шукати в пакеті [spring.cors.server.service];
  • рядок 14: імпортуються біни з проєкту [spring-security-server-jdbc-generic];
  • рядки 18–21: ми створюємо компонент Spring із назвою [isCorsEnabled], який визначає, чи приймаються клієнти, що не належать до домену сервера;

21.2.9. Контролери

Новий веб-сервіс має чотири контролери:

  
  • [CorsCategorieController] керує URL, що обробляє категорії. Він обробляє лише заголовки CORS від веб-клієнтів. В іншому випадку він делегує роботу контролеру [CategorieController] залежності [spring-webjson-server-jdbc-generic];
  • [CorsProduitController] та [CorsAuthenticateController] діють аналогічно, делегуючи роботу контролерам [ProduitController] залежності [spring-webjson-server-jdbc-generic] та [AuthenticateController] залежності[spring-security-server-jdbc-generic];
  • [CorsController] слугує для виокремлення спільних елементів трьох попередніх контролерів;

21.2.9.1. Контролер [CorsController]

Клас [CorsController] має такий вигляд:


package spring.cors.server.service;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CorsController {

    @Autowired
    private boolean isCorsEnabled;

    // надсилання параметрів клієнту
    public void sendOptions(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");
    }
}
  • рядок 8: клас [CorsController] є контролером Spring;
  • рядки 11–12: ін’єкція біна [isCorsEnabled], який вказує, чи слід обробляти заголовки CORS;
  • рядки 15–26: метод [sendOptions] відповідає клієнтам, які надсилають заголовки CORS;
  • рядки 17–19: якщо додаток налаштовано на прийом міждоменних запитів, і якщо відправник надіслав заголовок HTTP [Origin], і якщо це джерело починається з [http://localhost], то міждоменний запит буде прийнято, інакше — відхилено;
  • рядок 21: якщо клієнт перебуває в домені [http://localhost:port], надсилається заголовок HTTP:
Access-Control-Allow-Origin:  http://localhost:порт

що означає, що сервер приймає домен клієнта;

  • рядки 22–25: ми вказали два особливі заголовки HTTP у запиті HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

На заголовок HTTP [Access-Control-Request-X] сервер відповідає заголовком HTTP [Access-Control-Allow-X], у якому вказує, що дозволено. У рядках 22–25 просто повторюється запит клієнта, щоб вказати, що він прийнятий;

21.2.9.2. Контролер [CorsCategorieController]


package spring.cors.server.service;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
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.RestController;

import spring.jdbc.entities.Categorie;
import spring.webjson.server.entities.CoreCategorie;
import spring.webjson.server.service.CategorieController;
import spring.webjson.server.service.Response;

@RestController
public class CorsCategorieController extends CorsController {

    @Autowired
    private CategorieController categorieController;

    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.OPTIONS)
    public void corsGetAllShortCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
    }

    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.GET)
    public Response<List<Categorie>> getAllShortCategories(
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // метод походження
        return categorieController.getAllShortCategories();
    }

...
}
  • рядок 19: анотація [@RestController] робить клас одночасно компонентом Spring і контролером MVC, який сам надсилає свої відповіді клієнту;
  • рядок 20: клас [CorsCategorieController] успадковує клас [CorsController], який ми щойно розглянули;
  • рядки 22–23: ін'єкція контролера [CategorieController categorieController] із залежності [spring-webjson-server-jdbc-generic];
  • рядки 25–29: обробляють URL [/cors-getAllShortCategories], коли його викликають за допомогою команди HTTP [OPTIONS]. За домовленістю ми вирішили, що веб-клієнти, які хочуть викликати URL [/U] із захищеного веб-сервісу, насправді повинні викликати URL [/cors-U]. Таким чином, розгорнутий веб-сервіс матиме два типи URL:
    • [/U]: для клієнтів, що не використовують веб-інтерфейс;
    • [/cors-U] — для веб-клієнтів;
  • рядок 25: метод [/cors-getAllShortCategories] приймає як параметри:
    • об'єкт [@RequestHeader(value = "Origin", required = false)], який отримає заголовок HTTP [Origin] із запиту. Цей заголовок був надісланий відправником запиту:
Origin:http://localhost:8082

Зазначено, що заголовок HTTP [Origin] є необов’язковим [required = false]. У цьому випадку, якщо заголовок відсутній, параметр [String origin] матиме значення null. З [required = true], що є значенням за замовчуванням, у разі відсутності заголовка генерується виняток. Ми прагнули уникнути такого випадку;

  • (продовження)
    • об’єкт [HttpServletResponse response], який буде повернуто клієнту, що зробив запит;

Ці два параметри вводяться Spring;

  • рядок 28: обробку запиту делегуємо методу [sendOptions] батьківського класу [CorsController];
  • рядки 31–36: метод [getAllShortCategories] обробляє URL та [/cors-getAllShortCategories], коли його викликають із GET;
  • рядок 35: завдання делегується методу [CategorieController.getAllShortCategories] залежності [spring-webjson-server-jdbc-generic];

Тепер ми готові до нових тестів. Запускаємо нову версію веб-сервісу і виявляємо, що проблема залишається невирішеною. Нічого не змінилося. Якщо у рядку 28 вище розмістити вивід у консоль, він ніколи не відображається, що свідчить про те, що метод [corsGetAllShortCategories] у рядку 25 ніколи не викликається.

Після деяких досліджень ми з’ясували, що Spring MVC самостійно обробляє команди HTTP та [OPTIONS], застосовуючи обробку за замовчуванням. Тому відповідь завжди надає Spring, а не метод [corsGetAllShortCategories] у рядку 25. Цю поведінку 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;

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

    // міждоменні запити
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
    
    @Autowired
    private DispatcherServlet dispatcherServlet;
    
    @PostConstruct
    public void init(){
        // додаток самостійно обробляє запити HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}

  • рядки 23–24: вводимо компонент [DispatcherServlet dispatcherServlet], який було визначено у залежності [spring-webjson-server-jdbc-generic];
  • рядки 26–30: анотація [@PostConstruct] забезпечує виконання методу [init] після інстанціювання класу [AppConfig] та після ін'єкцій, виконаних Spring;
  • рядок 29: задано, щоб сервлет перенаправляв до додатка команди HTTP та [OPTIONS];

Ми повторюємо тести з цією новою конфігурацією. Отримуємо такий результат:

  • у [1] ми бачимо, що є два запити HTTP до URL та [http://localhost:8080/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].

Змінюємо метод, який обробляє GET, на URL [/cors-getAllShortCategories]:


    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.GET)
    public Response<List<Categorie>> getAllShortCategories(
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // заголовки CORS
        sendOptions(origin, response);
        // метод походження
        return categorieController.getAllShortCategories();
}
  • рядок 5: як і для запиту HTTP [OPTIONS], сервер надішле заголовки HTTP CORS для запиту HTTP [GET];

Після цієї зміни результати такі:

 

Ми дійсно отримали скорочену версію всіх категорій.

21.2.9.3. URL [GET]

У контролерах [CorsCategorieController, CorsProduitController, CorsAuthenticateController], код дій, що обробляють запити URL із [GET], відповідає шаблону дій, які раніше обробляли URL та [/cors-getAllShortArticles]. Читач може перевірити код у прикладах, що додаються до цього документа. Ось приклад для URL та [/cors-getAllLongProduits] контролера [CorsProduitController]:


package spring.cors.server.service;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
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.RestController;

import spring.jdbc.entities.Produit;
import spring.webjson.server.entities.CoreProduit;
import spring.webjson.server.service.ProduitController;
import spring.webjson.server.service.Response;

@RestController
public class CorsProduitController extends CorsController {

    @Autowired
    private ProduitController produitController;

@RequestMapping(value = "/cors-getAllLongProduits", method = RequestMethod.GET)
    public Response<List<Produit>> getAllLongProduits(@RequestHeader(value = "Origin", required = false) String origin,HttpServletResponse response) {
        // заголовки CORS
        sendOptions(origin, response);
        // метод походження
        return produitController.getAllLongProduits();

    }

    @RequestMapping(value = "/cors-getAllLongProduits", method = RequestMethod.OPTIONS)
    public void corsGetAllLongProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
    }
...
}
 

21.2.9.4. URL [POST]

Розглянемо такий випадок:

  • здійснюється перехід від POST [1] до URL [2];
  • у [3] — опубліковане значення. Це рядок jSON категорії без товарів;
  • у підсумку ми прагнемо створити категорію з назвою [categorie[2]];

Наразі ми не змінюємо жодного коду. Отриманий результат виглядає так:

  • у [1], як і для запитів [GET], браузер надсилає запит [OPTIONS];
  • у випадку [2] він запитує дозвіл на доступ для запиту [POST]. Раніше це був [GET];
  • у [3] він запитує дозвіл на відправку заголовків HTTP та [accept, authorization, content-type]. Раніше були лише перші два заголовки;
  • у [4] веб-сервіс не надає всіх запитуваних дозволів, що спричиняє помилку [5];

Ми змінюємо метод [CorsController.sendOptions] наступним чином:


    public void sendOptions(String origin, HttpServletResponse response) {
        // Чи дозволено тіло повідомлення?
        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");
    }
}
  • рядок 9: додано заголовок HTTP [Content-Type] (регістр не має значення);
  • рядок 11: додано метод HTTP [POST];

Таким чином, методи [POST] обробляються так само, як і запити [GET]. Ось приклад URL та [/cors-saveCategories] у контролері [CorsCategorieController]:


    @RequestMapping(value = "/cors-saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // заголовки CORS
        sendOptions(origin, response);
        // метод походження
        return categorieController.saveCategories(request);
    }

    @RequestMapping(value = "/cors-saveCategories", method = RequestMethod.OPTIONS)
    public void corsSaveCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
}

Після внесення цих змін отримано такий результат:

 

Категорія [categorie[2]] була успішно додана до бази даних. SGBD присвоїв їй первинний ключ 226. Це можна перевірити за допомогою методу GET [/cors-getAllShortCategories]:

 

21.2.10. Висновок

Наша програма тепер підтримує міждоменні запити. Їх можна дозволити або заборонити за допомогою налаштувань у класі [AppConfig]:


package spring.cors.server.config;

...

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

    // міждоменні запити
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
...
}

21.3. Проєкт Eclipse [spring-cors-server-jpa-generic]

Веб-сервіс CORS тепер буде реалізовано в рамках проєкту [spring-cors-server-jpa-generic], який базується на проєкті [spring-security-server-jpa-generic], що керує доступом до бази даних за допомогою Spring Data JPA:

Проєкт [spring-cors-server-jpa-generic] отримано шляхом копіювання раніше розглянутого проєкту [spring-cors-server-jdbc-generic].

  

Далі потрібно внести дві зміни. Перша — у файлі [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>dvp.spring.database</groupId>
    <artifactId>spring-cors-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-cors-server-jpa-generic</name>
    <description>démo spring cors</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <!-- плагіни -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
  • рядки 30–32: залежність від захищеного веб-сервісу [spring-security-server-jpa-generic];

У підсумку залежності проекту виглядають так:

  

Примітка: натисніть Alt-F5, а потім перегенеруйте всі проекти

Друга зміна полягає в оновленні імпортів у класах, що сигналізують про помилки [Alt-Maj-O].

Ось і все. Запускаємо веб-сервіс CORS із конфігурацією виконання [spring-cors-server-jpa-generic-hibernate-eclipselink]:

Потім запускаємо загальний клієнт:

і за допомогою браузера запитуємо URL, [1] та GET. У [2] видно, що коротка версія категорій містить поле [entityType], якого не було в попередній версії JDBC.

Ми розглянемо ще дві архітектури CORS:

  • архітектура CORS / JPA EclipseLink / DB2;
  • архітектура CORS / JPA OpenJpa / Firebird;

Ми реалізуємо таку архітектуру:

Завантажуємо такі проекти:

  

Примітка: натисніть Alt-F5 і перегенеруйте всі проекти Maven.

Запустіть SGBD та DB2 і переконайтеся, що база даних [dbproduitscategories] існує. Якщо ні, створіть її (параграф 12.1.2).

Створення користувачів у базі даних [dbproduitscategories] за допомогою конфігурації виконання [spring-security-create-users-hibernate-eclipselink]:

Image

Потім запустіть веб-сервіс CORS із конфігурацією виконання під назвою [spring-cors-server-jpa-generic-hibernate-eclipselink] та його клієнтом під назвою [spring-cors-client-generic]:

Заповніть базу даних [dbproduitscategories] значеннями, використовуючи конфігурацію виконання [spring-jdbc-generic-04-fillDataBase]:

 

Нарешті, завантажте в браузері наступний файл URL:

 

21.5. Архітектура CORS / JPA OpenJPA / Firebird

Тепер ми реалізуємо таку архітектуру:

Завантажуємо такі проекти:

  

Примітка: натисніть Alt-F5 і перегенеруйте всі проекти Maven.

Запустіть SGBD Firebird і переконайтеся, що база даних [dbproduitscategories] існує. Якщо ні, створіть її (параграф 14.1.2).

Створюємо користувачів у базі даних [dbproduitscategories] за допомогою конфігурації виконання [spring-security-create-users-openjpa]:

Image

Потім запустіть веб-сервіс CORS із конфігурацією виконання під назвою [spring-cors-server-jpa-generic-openjpa]:

Запустіть клієнт CORS із конфігурацією [spring-cors-client-generic]:

 

Заповніть базу даних [dbproduitscategories] значеннями, використовуючи конфігурацію виконання [spring-jdbc-generic-04-fillDataBase]:

 

Нарешті, завантажте в браузері наступний файл URL: