Настройка CORS в Spring Security

В целях безопасности браузер запрещает JS-скрипту  одного сайта обращаться на другой сайт без специального разрешения. Разрешение это реализуется с помощью технологии CORS (Cross-Origin Resource Sharing). Рассмотрим пример.

Когда возникает проблема

Создадим простое Spring Boot приложение с единственным Rest-контроллером:

@RestController
public class HelloController {

    @GetMapping("/api/hello")
    public String hello(){
        return "Hello";
    }
}
Зависимость spring-boot-starter-security пока не будем добавлять в проект. Достаточно spring-boot-starter-web.

В браузере запрос http://localhost:8080/api/hello выполняется успешно:

В браузере все ок.

Теперь запустим на другом порту localhost:4200 Angular-проект (статика и js), он будет выполнять ajax-запросы к нашему контроллеру.

Не обязательно проект должен быть на Angular, можно взять любой другой js-фреймворк или просто JQuery. Главное, мы имитируем обращение с другого сайта, то есть он должен быть на отдельном порту, не на 8080. Политика браузера такова, что другой порт для него считается такой же опасностью, как другой сайт.

Мы запустили Spring Boot приложение на localhost:8080, а Angular-клиент на порту localhost:4200.

Откроем в браузере localhost:4200  — откроется страница Angular-клиента, которая содержит JavaScript-код,  выполняющий запрос http://localhost:8080/api/hello. В консоли мы увидим ошибку, что-то вроде:

Ошибка в консоли браузера

Цитирую текстом:

Access to XMLHttpRequest at 'http://localhost:8080/api/hello' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Страница, естественно, не отображается. У новичка, не знакомого с CORS-политикой браузера, возникает искушение отключить весь CORS в Spring Security. Но у нас он еще не включен. Потому что дело не в  Spring Security, а в браузере. И CORS придется не отключить, а наоборот, включить. И настроить.

Включение CORS

Еще раз вернемся к ошибке:

No 'Access-Control-Allow-Origin' header is present on the requested resource.

Тут говорится, что в http-ответе на запрос http://localhost:8080/api/hello отсутствует заголовок Access-Control-Allow-Origin.

Что должно быть в http-ответе

Итак, чтобы браузер не выдавал ошибку, надо чтобы Spring Boot приложение явно указывало в http-ответе в заголовке Access-Control-Allow-Origin домен(ы), с которого разрешены запросы. То есть, чтобы не отсекать нашего клиента, запущенного на  http://localhost:4200, в ответе должен содержаться такой заголовок:

Access-Control-Allow-Origin: http://localhost:4200

Подразумевается, что Spring Boot приложение должно знать этот сайт http://localhost:4200 и давать ему разрешение на запросы с помощью вышеприведенного заголовка. Spring Boot приложение должно включать  этот заголовок в ответ.

Если бы наш Angular-клиент находился на сайте myangularclient.com, то ему требовался бы соответственно такой заголовок:

Access-Control-Allow-Origin: myangularclient.com

А такой заголовок со звездочкой разрешит запросы всем сайтам:

Access-Control-Allow-Origin: *

Но последнее не безопасно.

Итак, займемся добавлением заголовка.

Настройка Spring Security

Для начала добавим Spring Security в проект:

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

Как только это сделано, включится принудительная аутентификация всех запросов по умолчанию,  и при вводе в браузере запроса мы будем перенаправлены  на страницу логина:

Автоматически включилась аутентификация

Исправим это, разрешив все запросы для всех пользователей (наш фокус  — CORS, а не аутентификация):

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter  {
    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests().antMatchers("/**").permitAll();
    }
}

Теперь в браузере запрос http://localhost:8080/api/hello выполняется — возвращается hello, а вот при выполнении запроса с клиента возникает та же ошибка. Исправим ее.

Настройка CORS

Для этого реализуем еще метод addCorsMappings() интерфейса WebMvcConfigurer:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests().antMatchers("/**").permitAll();
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:4200")
                .allowedMethods("*");
    }
}

В методе мы говорим, каким сайтам по каким url и какими методами можно делать запросы к нашему приложению, запущенному на 8080.

Мы сказали, что:

  • только сайт http://localhost:4200 может делать запросы
  • запрос можно делать абсолютно всеми методами (GET, POST, PUT и т.д.)
  • обращаться к нашему приложению можно по любому внутреннему url — addMapping(«/**»)

Теперь проверим, как это работает с Angular-клиента.

Проверка

На картинке всплывающая подсказка говорит, что мы обращаемся по запросу

http://localhost:8080/api/hello

Запрос и ответ содержат такие заголовки (запрос смотрите внизу, а ответ вверху, важное выделено красным):

Просмотр запроса и ответа в Chrome Developer Tools, вкладка Network
Просмотр запроса и ответа в Chrome Developer Tools, вкладка Network
Запрос
Host: localhost:8080
Origin: http://localhost:4200

В запросе в заголовках Origin и Host содержится откуда и куда идет запрос. По ним приложение Spring Boot поймет, надо ли вообще включать в ответ заголовок Access-Control-Allow-Origin (если Origin и Host одинаковые, то не надо, проблемы в браузере в этом случае в принципе не возникнет).

Заголовок Origin браузер добавляет автоматически, JS-программист не может его подделать.
Ответ

Далее наше Spring Boot приложение понимает, сайту http://localhost:4200 доступ разрешен (это мы настроили в методе addCorsMappings), так что в http-ответ оно включает такой заголовок:

Access-Control-Allow-Origin: http://localhost:4200

Браузер видит по этому заголовку, что сайту http://localhost:4200 доступ разрешен и не зажёвывает ответ с выбросом ошибки в консоль, как это делал раньше, а передает его клиенту. Все ок.

Исходный код Spring Boot примера (без Angular-клиента) доступен на GitHub.

Настройка CORS в Spring Security: 10 комментариев

  1. Спасибо огромное за статью!
    Перерыл кучу ресурсов, перепробовал много всего и потратил много часов — и только предложенное здесь решение оказалось полноценным решением проблемы.

    1. Да, автор этого сайта просто снимает всю головную боль! 🙂
      Вопрос к автору:
      Если заголовок Access-Control-Allow-Origin: * обрабатывается браузером, то как например будет идти обработка запроса любым другим Rest клиентом (например вредоносным с подделкой заголовков)? Как тогда защититься? Так же могут за кршпулить api сайта (если не включать безопасность, jwt например).

      1. Вопрос не правильно поставлен. Приложение не защищено, доступ к http://localhost:8080/api/hello можно получить из любого клиента, не надо даже подделывать никакие заголовки. А вот из браузера нельзя (если обращаться с другого домен c помощью JScript). И в статье речь о том, как снять защиту, выставляемую браузером, чтобы таки получить доступ с одного нужного домена http://localhost:4200.

  2. Можно и так:

    @RestController
    @CrossOrigin(origins=»http://localhost:4200″)
    @RequestMapping(«api»)
    public class HelloController {

    @GetMapping(«hello»)
    public String hello(){
    return «Hello»;
    }
    }

  3. Спасибо автору! Действительно полезная информация.
    К сожалению, я так и не смог победить решение с классом конфигурации, контекст просто не поднимался
    2021-01-13 22:04:30.911 ERROR 5092 — [ restartedMain] o.s.boot.SpringApplication : Application run failed
    nested exception is java.io.FileNotFoundException: class path resource [org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.class] cannot be opened because it does not exist
    упорно не находит класс, хотя пробовал уже разные танцы с бубном. В итоге решение с аннотацией @CrossOrigin(origins=”http://localhost:4200″)
    над классом контроллера работает.

  4. СПАСИБИЩЕ вам! Просто огромный респект. Помогло и стало понятно, что к чему с этим CORS!

  5. Полностью скопировал ваш файл SecurityConfig ошибка CORS ушла. Но теперь выдает ошибку 403
    error: «Forbidden»
    status: 403
    Не пойму в чем проблема, почему в вас работает.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *