Учебные материалы

Активация CORS для RESTful Web-сервиса

Этот урок освещает создание "hello world" RESTful web сервиса с использованием Spring, который включает заголовки для Cross-Origin Resource Sharing (CORS) в ответе.

Что вы создадите

Вы создадите сервис, который будет принимать HTTP GET запросы на:

http://localhost:8080/greeting

а ответом будет JSON строка вида:

{"id":1,"content":"Hello, World!"}

Вы можете модифицировать сообщение ответа, если добавить к строке запроса параметр name:

http://localhost:8080/greeting?name=User

Значение параметра name заменяет значение по умолчанию "World" отображается в ответе:

{"id":1,"content":"Hello, User!"}

Этот сервис немного отличается от описанного в Создание RESTful Web-сервиса тем, что он имеет фильтр, который добавляет CORS заголовки к ответу.

Что вам потребуется

  • Примерно 15 минут свободного времени
  • Любимый текстовый редактор или IDE
  • JDK 6 и выше
  • Gradle 1.11+ или Maven 3.0+
  • Вы также можете импортировать код этого урока, а также просматривать web-страницы прямо из Spring Tool Suite (STS), собственно как и работать дальше из него.

Как проходить этот урок

Как и большинство уроков по Spring, вы можете начать с нуля и выполнять каждый шаг, либо пропустить базовые шаги, которые вам уже знакомы. В любом случае, вы в конечном итоге получите рабочий код.

Чтобы начать с нуля, перейдите в Настройка проекта.

Чтобы пропустить базовые шаги, выполните следующее:

Когда вы закончите, можете сравнить получившийся результат с образцом в gs-rest-service-cors/complete.

Настройка проекта

Для начала вам необходимо настроить базовый скрипт сборки. Вы можете использовать любую систему сборки, которая вам нравится для сборки проетов Spring, но в этом уроке рассмотрим код для работы с Gradle и Maven. Если вы не знакомы ни с одним из них, ознакомьтесь с соответсвующими уроками Сборка Java-проекта с использованием Gradle или Сборка Java-проекта с использованием Maven.

Создание структуры каталогов

В выбранном вами каталоге проекта создайте следующую структуру каталогов; к примеру, командой mkdir -p src/main/java/hello для *nix систем:

└── src
    └── main
        └── java
            └── hello

Создание файла сборки Gradle

Ниже представлен начальный файл сборки Gradle. Файл pom.xml находится здесь. Если вы используете Spring Tool Suite (STS), то можете импортировать урок прямо из него.

Если вы посмотрите на pom.xml, вы найдете, что указана версия для maven-compiler-plugin. В общем, это не рекомендуется делать. В данном случае он предназначен для решения проблем с нашей CI системы, которая по умолчанию имеет старую(до Java 5) версию этого плагина.

build.gradle

buildscript {
    repositories {
        maven { url "https://repo.spring.io/libs-release" }
        mavenLocal()
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.9.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'gs-rest-service-cors'
    version =  '0.1.0'
}

repositories {
    mavenLocal()
    mavenCentral()
    maven { url "https://repo.spring.io/libs-release" }
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("junit:junit")
}

task wrapper(type: Wrapper) {
    gradleVersion = '1.11'
}

Spring Boot gradle plugin предоставляет множество удобных возможностей:

  • Он собирает все jar'ы в classpath и собирает единое, исполняемое "über-jar", что делает более удобным выполнение и доставку вашего сервиса
  • Он ищет public static void main() метод, как признак исполняемого класса
  • Он предоставляет встроенное разрешение зависимостей, с определенными номерами версий для соответсвующих Spring Boot зависимостей. Вы можете переопределить на любые версии, какие захотите, но он будет по умолчанию для Boot выбранным набором версий

Создание класса, представляющего ресурс

Теперь, когда вы настроили проект и систему сборки, вы можете создать ваш web-сервис.

Для начала подумаем о взаимодействии с сервисом.

Сервис будет обрабатывать GET запросы для /greeting, дополнительно - параметр name в строке запроса. GET запрос должен возвращать ответ со статусом 200 OK и JSON в теле, которое соответствует сообщению приветствия. он должен выглядеть таким образом:

{
    "id": 1,
    "content": "Hello, World!"
}

Поле id - это уникальный идентификатор приветствия, а content - текстовое представление.

Для модели представления приветствия вам необходимо создать класс, представляющего ресурс. Он представляет собой простой java-объект с полями, конструкторами и методами доступа к значениям id и content:

src/main/java/hello/Greeting.java

package hello;

public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}
Как вы можете заметить ниже, Spring использует Jackson JSON, библиотеку для автоматического разбора экземпляров типа Greeting в JSON.

Далее вы создаете контроллер ресурса, который будет обрабатывать эти приветствия.

Создание контроллера ресурса

Подход Spring при построении RESTful web-сервиса заключается в обработке HTTP-запосов контроллером. Эти компоненты легко опознаются по @RestController аннотации, а GreetingController ниже обрабатывает GET запросы для /greeting, возвращая новый экземпляр Greeting класса:

src/main/java/hello/GreetingController.java

package hello;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class GreetingController {

    private static final String template = "Hello, %s!";
    private final AtomicLong counter = new AtomicLong();

    @RequestMapping("/greeting")
    public @ResponseBody Greeting greeting(
            @RequestParam(value="name", required=false, defaultValue="World") String name) {
    	System.out.println("==== in greeting ====");
        return new Greeting(counter.incrementAndGet(),
                            String.format(template, name));
    }
}

Этот контроллер краток и прост, однако внутри происходит много чего. Давайте разберем его шаг за шагом.

Аннотация @RequestMapping гарантирует, что HTTP-запросы на /greeting будут сопоставлены greeting() методу.

В примере выше не указывается GET, PUT, POST и т.д., потому то @RequestMapping сопоставляет все HTTP-операции по умолчанию. Использование @RequestMapping(method=GET) определяет это сопоставление.

@RequestParam связывает значение строкового параметра name запроса с параметром name метода greeting(). Этот строковый параметр запроса не required(не обязателен); если он отсутствует в запросе, то будет использовано defaultValue "World".

Реализация метода создает и возвращает новый Greeting объект с атрибутами id и content, основанными на следующем значении counter и форматированном значении name по шаблону template.

Ключевое отличие можду традиционным MVC контроллером и контроллером RESTful web-сервиса выше в создании тела HTTP-ответа. Вместо того, чтобы опираться на view-технологию для рендеринга на серверной стороне сообщения приветствия в HTML, RESTful web-сервис контроллер просто заполняет и возвращает Greeting объект. Данные объекта будут записаны напрямую в HTTP-ответ как JSON.

Аннотация @ResponseBody в методе greeting() говорит Spring MVC, что не нужно отображать объект приветствия через слой представления на стороне сервера, а вместо этого писать его напрямую в телоответа.

Greeting объект должен быть конвертирован в JSON. Благодаря поддержке Spring HTTP конвертера, вам не требуется выполнять эту конвертацию вручную. Когда Jackson 2 в classpath, MappingJackson2HttpMessageConverter из Spring выбирается автоматически для конвертации экземпляра Greeting в JSON.

Фильтр запросов CORS

Т.к. RESTful web сервис будет включать CORS заголовки контроля доступа в свой ответ, вам необходимо написать фильтр, который добавляет заголовки к ответу. Ниже приведенный класс SimpleCORSFilter обеспечивает простой реализацией для такого фильтра:

src/main/java/hello/SimpleCORSFilter.java

package hello;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;

@Component
public class SimpleCORSFilter implements Filter {

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		HttpServletResponse response = (HttpServletResponse) res;
		response.setHeader("Access-Control-Allow-Origin", "*");
		response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
		response.setHeader("Access-Control-Max-Age", "3600");
		response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
		chain.doFilter(req, res);
	}

	public void init(FilterConfig filterConfig) {}

	public void destroy() {}

}

Как уже было написано, SimpleCORSFilter отвечает на все запросы с определенными Access-Control-* заголовками. В данном случае, заголовки установлены на разрешение POST, GET, OPTIONS или DELETE запросов от клиентов любого хоста. Результат предполетного запроса может быть кеширован до 3600 секунд(1 час) и запрос может содержать x-requested-with заголовок.

Это всего лишь простой CORS фильтр. Более сложный фильтр может устанавливать значения заголовков, различные для передаваемому клиенту запросом ресурсу, либо не устанавливать заголовки в некоторых случаях.

Создание приложения исполняемым

Несмотря на то, что возможно упаковать этот сервис в традиционный WAR файл для разертывания на стороннем сервере приложений, более простой подход, продемонстрированный ниже создает отдельное самостоятельное приложение. Вы упаковываете все в единый, исполняемый JAR-файл, который запускается через хорошо знакомый старый main() Java-метод. Попутно, вы используете поддержку Spring для встроенного Tomcat контейнера сервлетов как HTTP среду выполнения вместо развертывания на сторонний экземпляр.

src/main/java/hello/Application.java

package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

main() метод передает управление вспомогательному классу SpringApplication, где Application.class - аргумент его run() метода. Это сообщает Spring о чтении метаданных аннотации из Application и управлении ею как компонента в Spring application context.

Аннотация @ComponentScan говорит Spring'у рекурсивно искать в пакете hello и его потомках классы, помеченные прямо или косвенно Spring аннотацией @Component. Эта директива гарантирует, что Spring найдет и зарегистрирует GreetingController, потому что он отмечен @RestController, который, в свою очередь, является своего рода @Component аннотацией.

Аннотация @EnableAutoConfiguration переключает на приемлемое по умолчанию поведение, основанное на содержании вашего classpath. К примеру, т.к. приложение зависит от встраиваемой версии Tomcat (tomcat-embed-core.jar), Tomcat сервер установлен и сконфигурирован на приемлемое по умолчанию поведение от вашего имени. И т.к. приложение также зависит от Spring MVC (spring-webmvc.jar), Spring MVC DispatcherServlet сконфигурирован и зарегестрирован для вас - web.xml не нужен! Автоконфигурация полезный и гибкий механизм. Подробную информацию смотрите в API документации.

Сборка исполняемого JAR

Вы можете собрать единый исполняемый JAR-файл, который содержит все необходимые зависимости, классы и ресурсы. Это делает его легким в загрузке, версионировании и развертывании сервиса как приложения на протяжении всего периода разработки, на различных средах и так далее.

./gradlew build

Затем вы можете запустить JAR-файл:

java -jar build/libs/gs-rest-service-cors-0.1.0.jar

Если вы используете Maven, вы можете запустить приложение, используя mvn spring-boot:run, либо вы можете собрать приложение с mvn clean package и запустить JAR примерно так:

java -jar target/gs-rest-service-cors-0.1.0.jar
Процедура, описанная выше, создает исполняемый JAR. Вы также можете вместо него собрать классический WAR-файл.

Запуск сервиса

Если вы используете Gradle, вы можете запустить ваш сервис из командной строки:

./gradlew clean build && java -jar build/libs/gs-rest-service-cors-0.1.0.jar
Если вы используете Maven, то можете запустить ваш сервис таким образом: mvn clean package && java -jar target/gs-rest-service-cors-0.1.0.jar.

Как вариант, вы можете запустить ваш сервис напрямую из Gradle примерно так:

./gradlew bootRun
С mvn - mvn spring-boot:run.

Выходные данные отображены. Сервис должен быть поднят и запущен через несколько секунд.

Тестирование сервиса

Теперь, когда сервис поднят, поcетите http://localhost:8080/greeting и вы увидите:

{"id":1,"content":"Hello, World!"}

Добавьте строковый параметр name в запрос http://localhost:8080/greeting?name=User. Обратите внимание, как значение атрибута content изменится с "Hello, World!" на "Hello User!":

{"id":1,"content":"Hello, User!"}

Это изменение показывает, что аннотация @RequestParam в GreetingController работает, как и ожидалось. Параметру name было дано значение по умолчанию "World", но оно может быть всегда переопределено через строку запроса.

Обратите внимание, как значение атрибута id изменилось с 1 на 2. Это доказывает, что вы работаете с той же копией экземпляра GreetingController на протяжении нескольких запросов, и что это привело к увеличению значения поля counter при каждом вызове, как и ожидалось.

Теперь, чтобы протестировать что CORS заголовки на месте и позволяют JavaScript клиенту из другого источника получить доступ к сервису, вам необходимо создать JavaScript клиент.

Для начала создайте простой JavaScript файл hello.js со следующим содержанием:

public/hello.js

$(document).ready(function() {
    $.ajax({
        url: "http://localhost:8080/greeting"
    }).then(function(data, status, jqxhr) {
       $('.greeting-id').append(data.id);
       $('.greeting-content').append(data.content);
       console.log(jqxhr);
    });
});

Этот скрипт использует jQuery для обработки ответа от сервиса http://localhost:8080/greeting. Скрипт загружается на странице index.html:

public/index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Hello jQuery</title>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        <script src="hello.js"></script>
    </head>

    <body>
        <div>
            <p class="greeting-id">The ID is </p>
            <p class="greeting-content">The content is </p>
        </div>
        <h4>Response headers:</h4>
        <div class="response-headers">
        </div>
    </body>
</html>
По факту, это тот же клиент, что и в Обработка ответа RESTful Web-сервиса JQuery'ом, немного модифицированный на обработку ответа от сервиса, запущенном на localhost на 8080 порту.

Чтобы запустить клиент, вам необходимо поднять web сервер. Spring Boot CLI (Command Line Interface) включает встроенный Tomcat сервер, который предоставляет простой подход к обработке web содержимого. Более подробную информацию об установке и использовании CLI смотрите в учебном материале Создание приложений с Spring Boot.

Для обработки статического содержимого вам необходимо создать минимальное количество кода. Приведенный ниже app.groovy скрипт достаточен для того, чтобы Spring Boot знал, что вы хотите запустить Tomcat:

app.groovy

@Controller class JsApp { }

Т.к. REST сервис уже запущен на localhost на 8080 порту, вам необходимо обязательно запустить клиента с другого сервера и/или порта. Это позволит избежать не только коллизий между двумя приложениями, но и быть уверенным в том, что клиентский код будет запущен с другого источника, отличного от источника сервиса. Для запуска клиента на localhost на 9000 порту, выполните:

spring run app.groovy -- --server.port=9000

Как только клиент запустится, откройте http://localhost:9000/ в вашем браузере, где вы увидите следующее:

Если ответ сервиса включает CORS заголовки, то на странице будут отображены ID и content. Но если CORS заголовки отсутствуют(или недостаточно определены для клиента), то браузер получит ошибку запроса и значения не будут отображены:

Итог

Поздравляем! Вы только что создали RESTful web-сервис, включающий Cross-Origin Resource Sharing с Spring.

С оригинальным текстом урока вы можете ознакомиться на spring.io.

comments powered by Disqus