Skip to content

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

Image

Завантажений файл слід помістити в папку [static / js] і змінити рядок 6 у файлі HTML відповідно до встановленої версії.

Після цього відкриваємо статичний вигляд [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 у браузері не змінилося під час усіх цих операцій. Обміну даними з веб-сервером не відбувалося. Усе відбувається всередині браузера. Тепер переглянемо вихідний код сторінки:

Це початковий текст. Він жодним чином не відображає маніпуляції, які ми виконали з елементом у рядках 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:http://localhost:8081

У цьому випадку параметр [origin] у рядку 13 матиме значення [http://localhost:8081]. У разі, якщо запит не містить заголовка HTTP [Origin], ми подбаємо про те, щоб отримати [origin==null];

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

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

  • рядки 15–175: якщо додаток налаштовано на прийом міждоменних запитів, і якщо відправник надіслав заголовок HTTP [Origin], і якщо це джерело починається з [http://localhost], то міждоменний запит буде прийнято, інакше — відхилено;
  • рядок 19: якщо клієнт знаходиться в домені [http://localhost:port], надсилається заголовок HTTP:
Access-Control-Allow-Origin:  http://localhost:порт

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

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

На заголовок 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);
    }
}