О CORS
2018-12-08
Современные браузеры не хотят просто так ходить на другие домены. Точнее на другой Origin. Это касается JavaScript, выполняемого в браузере.
Нельзя просто так взять, и потыкать API, находящееся в другом домене. И нельзя сделать crawler, работающий в браузере. Это будут cross-origin запросы. И они запрещены. Из соображений безопасности. Похоже, это скорее забота о том, чтобы конфиденциальные данные из вашего браузера не утекли куда не надо, чем попытка затруднить жизнь тем, кто захочет воспользоваться вашим API.
Но иногда у вас какой-нибудь SPA на React должен ходить в API на другой домен. И тут на помощь приходит CORS. Cross-Origin Resource Sharing. Сервер другого ресурса, нашего API, может явно сказать, с каких Origin к нему разрешён доступ. С помощью некоторых дополнительных HTTP заголовков.
В простейшем случае,
когда наш JavaScript делает GET запрос,
и не передаёт никаких
потенциально опасных заголовков,
браузер включает в запрос заголовок Origin
.
А сервер должен ответить
с заголовком Access-Control-Allow-Origin
,
где,
собственно,
и говорит,
разрешён ли доступ с этого Origin.
В более сложном случае, например, когда мы делаем POST запрос с JSON в теле запроса, браузер делает так называемый "preflight request".
Это OPTIONS запрос,
в который включены заголовки,
описывающие будущий полноценный запрос.
Access-Control-Request-Method
говорит,
какой HTTP метод будет в будущем запросе.
Access-Control-Request-Headers
говорит,
какие HTTP заголовки будут в будущем запросе
(помимо тех, вроде Host
, User-Agent
и Acept
,
которые и так разрешены по умолчанию).
Конечно же,
этот запрос
включает заголовок Origin
.
Если сервер согласен
и с Origin, и с методом, и с заголовками,
он отвечает 200 OK
с дополнительными заголовками.
Снова заголовок Access-Control-Allow-Origin
выражает согласие сервера с Origin.
Заголовок Access-Control-Allow-Methods
перечисляет методы,
которые сервер готов принимать с этого Origin.
Заголовок Access-Control-Allow-Headers
подтверждает готовность сервера принимать заголовки.
Заголовок Access-Control-Max-Age
указывает срок действия данного соглашения,
браузер может какое-то время
не повторять preflight запросы.
В Access-Control-Allow-Origin
сервер может вернуть не только тот самый Origin,
что запросил доступ,
но и специальное значение "*",
звёздочку.
Это означает "любой Origin".
Но в этом случае запрещается передавать в запросе
credentials.
Под "credentials" подразумеваются куки,
информация об аутентификации,
включая заголовки и TLS сертификаты.
Всё то,
что браузер может неявно подмешать в запрос,
и что явно идентифицирует пользователя.
По умолчанию credentials запрещены.
Но сервер может разрешить,
если выдаст заголовок
Access-Control-Allow-Credentials
со значением true
.
Но для Origin "*" это не работает.
Если говорить про
fetch()
в браузере,
то для credentials есть свои опции.
fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, cors, *same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json; charset=utf-8",
// "Content-Type": "application/x-www-form-urlencoded",
},
redirect: "follow", // manual, *follow, error
referrer: "no-referrer", // no-referrer, *client
body: JSON.stringify(data), // body data type must match "Content-Type" header
})
Сервер может ещё использовать заголовок
Vary
,
чтобы указать на заголовки запроса,
изменение которых приведёт к другому ответу сервера.
Поэтому положено,
если Access-Control-Allow-Origin
возвращает конкретный Origin,
то должен быть ещё и Vary: Origin
.
Почти во всех случаях, когда у вас есть SPA и отдельное API, вам придётся учитывать CORS.
Вам повезло, если вы можете закрыть и фронтенд и бэкенд общим прокси на одном домене. Тогда ваше API будет, скажем, просто по относительному URL "/api", и это будет same-origin, и CORS не понадобится.
Вам повезло,
если перед бэкендом вы можете поставить прокси,
которое сможет добавлять заголовки.
Например, Nginx.
Тогда он сможет взять на себя всю работу c CORS.
Не забудьте включить в Access-Control-Allow-Headers
все заголовки,
которые рожает ваше SPA.
location /api {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Origin,Accept,Key,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin,Accept,Key,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
proxy_pass http://127.0.0.1:8080/;
}
Если вам позарез нужны credentials, например, аутентификация у вас через cookie, то всё немного усложняется.
location /api {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Vary' 'Origin';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Origin,Accept,Key,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Vary' 'Origin';
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin,Accept,Key,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
proxy_pass http://127.0.0.1:8080/api;
}
Как правило,
на preflight запрос сервер должен отвечать
минуя все механизмы авторизации.
Собственно,
так и произойдёт,
если вы к этому location добавите ещё,
например,
директивы auth_basic
.
Потому что return
прерывает обработку запроса раньше.
Приведённые конфигурации открывают доступ к API с любого Origin. Возможно, это не то, что вы хотите. Тогда разбираться с CORS придётся в самом бэкенде.
Если вы разворачиваетесь в облаке, то вряд ли вам захочется запускать свой Nginx, лучше воспользоваться облачным load balancer. А они вовсе не обязаны втыкать дополнительные заголовки. К тому же в облаках статику и динамику как-то удобнее размещать в разных доменах, CDN, всё такое. Так что от CORS не отвертитесь, и снова на бэкенде.
В Spring Boot можно сделать так:
@Configuration
@EnableConfigurationProperties(CorsProperties::class)
open class CorsConfiguration {
@Bean
open fun corsConfigurer(
corsProperties: CorsProperties?
): WebMvcConfigurer {
return object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
if (corsProperties != null && corsProperties.allowedOrigins.isNotEmpty()) {
registry.addMapping("/api/**").allowedOrigins(*corsProperties.allowedOrigins.toTypedArray())
}
}
}
}
}
@ConfigurationProperties(prefix = "web.cors")
open class CorsProperties {
var allowedOrigins: List<String> = mutableListOf()
}
Тогда разрешить нужные Origin
можно в application.yml
:
web:
cors:
allowed-origins:
- 'http://localhost:3000'
- 'http://my.webapp.com'