В целях безопасности браузер запрещает JS-скрипту одного сайта обращаться на другой сайт без специального разрешения. Разрешение это реализуется с помощью технологии CORS (Cross-Origin Resource Sharing). Рассмотрим пример.
Когда возникает проблема
Создадим простое Spring Boot приложение с единственным Rest-контроллером:
@RestController public class HelloController { @GetMapping("/api/hello") public String hello(){ return "Hello"; } }
В браузере запрос http://localhost:8080/api/hello выполняется успешно:
Теперь запустим на другом порту localhost:4200 Angular-проект (статика и js), он будет выполнять ajax-запросы к нашему контроллеру.
Мы запустили 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
Запрос и ответ содержат такие заголовки (запрос смотрите внизу, а ответ вверху, важное выделено красным):
Запрос
Host: localhost:8080 Origin: http://localhost:4200
В запросе в заголовках Origin и Host содержится откуда и куда идет запрос. По ним приложение Spring Boot поймет, надо ли вообще включать в ответ заголовок Access-Control-Allow-Origin (если Origin и Host одинаковые, то не надо, проблемы в браузере в этом случае в принципе не возникнет).
Ответ
Далее наше Spring Boot приложение понимает, сайту http://localhost:4200 доступ разрешен (это мы настроили в методе addCorsMappings), так что в http-ответ оно включает такой заголовок:
Access-Control-Allow-Origin: http://localhost:4200
Браузер видит по этому заголовку, что сайту http://localhost:4200 доступ разрешен и не зажёвывает ответ с выбросом ошибки в консоль, как это делал раньше, а передает его клиенту. Все ок.
Исходный код Spring Boot примера (без Angular-клиента) доступен на GitHub.
Спасибо огромное за статью!
Перерыл кучу ресурсов, перепробовал много всего и потратил много часов — и только предложенное здесь решение оказалось полноценным решением проблемы.
Да, автор этого сайта просто снимает всю головную боль! 🙂
Вопрос к автору:
Если заголовок Access-Control-Allow-Origin: * обрабатывается браузером, то как например будет идти обработка запроса любым другим Rest клиентом (например вредоносным с подделкой заголовков)? Как тогда защититься? Так же могут за кршпулить api сайта (если не включать безопасность, jwt например).
Вопрос не правильно поставлен. Приложение не защищено, доступ к http://localhost:8080/api/hello можно получить из любого клиента, не надо даже подделывать никакие заголовки. А вот из браузера нельзя (если обращаться с другого домен c помощью JScript). И в статье речь о том, как снять защиту, выставляемую браузером, чтобы таки получить доступ с одного нужного домена http://localhost:4200.
Спасибо, теперь понятно.
Можно и так:
@RestController
@CrossOrigin(origins=»http://localhost:4200″)
@RequestMapping(«api»)
public class HelloController {
@GetMapping(«hello»)
public String hello(){
return «Hello»;
}
}
да, можно.
Спасибо автору! Действительно полезная информация.
К сожалению, я так и не смог победить решение с классом конфигурации, контекст просто не поднимался
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″)
над классом контроллера работает.
СПАСИБИЩЕ вам! Просто огромный респект. Помогло и стало понятно, что к чему с этим CORS!
Полностью скопировал ваш файл SecurityConfig ошибка CORS ушла. Но теперь выдает ошибку 403
error: «Forbidden»
status: 403
Не пойму в чем проблема, почему в вас работает.
в примере permitAll() для всех url стоит, а у вас видимо защищен некий url