Fetch и CORS. Пример на ReactJS и другие

Fetch and CORS. React JS examples and etc. Fetch и кросс-доменные запросы. Пример на React JS и т.д.

Йо-йо! Недавно я столкнулся с задачей — создать сайт, на котором отображаются бонусы клиента. Бонусы я получаю с web-сервиса из 1С, он отдаёт мне json с данными, но предварительно там я должен авторизоваться.

Для создания этого сайта я решил использовать reactjs. Сайт довольно простой и многого там не будет, но нужно сделать авторизацию. Авторизация происходила с помощью передачи заголовка Authorization примерно вот так:

Authorization: Basic 0ZHJadMU0KI6

Но как только я решил сделать fetch и запросить данные, столкнулся со множеством проблем, в том числе CORS. Я решил эти проблемы и теперь хочу поделиться своим опытом.

Fetch и авторизация

Чтобы fetch мог передавать данные для авторизации нужно явно «сказать» ему, что в нём передаются данные для авторизации с помощью credentials. Пример

// Умышленно опускаю прочий код компонента, сейчас не важно.
componentDidMount(){
    const url = 'https://172.168.0.1/api/v1/methdeName?id="123213"';
    const headers = new Headers({
        'Authorization': '0ZHJadMU0KI6' 
    });
    const options = {
        headers,
        credentials:"include" // Вот, что нужно задать
    };
    fetch(url, options).then((response)=>{
        console.log(response.json());
    })
}

В этом случае всё пройдёт гладко и вы успешно пройдёте авторизацию.

Fetch и CORS

CORS сильно портит жизнь веб-мастеру, но зато защищает нас. Чтобы описать взаимодействие fecth и сервера в корсcдоменных запросах я приведу несколько примеров из спецификации fetch.

Пример 1

Скрипт на https: //foo.invalid/ хочет получить некоторые данные с https: //bar.invalid/. (Ни учетные данные, ни доступ к заголовку ответа не важны.)

var url = "https://bar.invalid/api?key=730d67a37d7f3d802e96396d00280768773813fbe726d116944d814422fc1a45&data=about:unicorn";
fetch(url).then(success, failure)

При этом будет использоваться протокол CORS, хотя он полностью прозрачен для разработчика из foo.invalid. Как часть протокола CORS пользовательский агент будет включать заголовок Origin в запрос:

Origin: https://foo.invalid

Получив ответ от bar.invalid, пользовательский агент проверит заголовок ответа «Access-Control-Allow-Origin». Если его значение равно https: // foo.invalid или *, пользовательский агент вызовет успешный обратный (success) вызов. Если оно имеет какое-либо другое значение или отсутствует, пользовательский агент вызовет failure.

Пример 2

Разработчик foo.invalid вернулся и теперь хочет получить некоторые данные из bar.invalid, одновременно обращаясь к заголовку ответа.

fetch(url).then(response => {
  var hsts = response.headers.get("strict-transport-security"),
      csp = response.headers.get("content-security-policy")
  log(hsts, csp)
})

bar.invalid предоставляет правильный заголовок ответа Access-Control-Allow-Origin в соответствии с предыдущим примером. Значения hsts и csp будут зависеть от заголовка ответа «Access-Control-Expose-Headers». Например, если в ответ включены следующие заголовки,

Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Access-Control-Expose-Headers: Content-Security-Policy

тогда hsts будет нулевым, а csp будет «default-src ‘self», даже если ответ включает оба заголовка. Это связано с тем, что bar.invalid должен явно разделять каждый заголовок, перечисляя их имена в заголовке ответа Access-Control-Expose-Headers.

В качестве альтернативы, если bar.invalid хочет совместно использовать все свои заголовки ответа для запросов, которые не включают учетные данные, он может использовать ‘*’ в качестве значения для заголовка ответа Access-Control-Expose-Headers. Если бы запрос включал учетные данные, имена заголовков ответов должны были бы быть перечислены явно, и ‘*’ не мог бы использоваться.

Пример 3. С передачей учётных данных (пароля)

Разработчик foo.invalid извлекает некоторые данные из bar.invalid, включая учетные данные. На этот раз протокол CORS больше не прозрачен для разработчика, поскольку учетные данные требуют явного согласия:

fetch(url, { credentials:"include" }).then(success, failure)

Это также делает все заголовки ответа Set-Cookie bar.invalid полностью функциональными (в противном случае они игнорируются).

Пользовательский агент обязательно включит в запрос все соответствующие учетные данные. Это также повысит требования к ответу. Мало того, что bar.invalid нужно будет перечислить https: // foo.invalid в качестве значения для заголовка Access-Control-Allow-Origin (‘*’ не допускается, когда задействованы учетные данные), Access-Control-Allow-Credentials заголовок также должен присутствовать:

Access-Control-Allow-Origin: https://foo.invalid
Access-Control-Allow-Credentials: true

Если ответ не включает эти два заголовка с этими значениями, будет вызван failure callback (в fetch). Однако любые заголовки ответа Set-Cookie будут соблюдены.

CORS, Fetch и сервер

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

// Нет авторизации
Access-Control-Allow-Origin: *
// Нужна авторизация, помним, что обызателен https на сервере и клиенте
Access-Control-Allow-Origin: https://example.ru 
Access-Control-Allow-Credentials: true
// Нужен доступ к заголовкам ответа
Access-Control-Expose-Headers: * 

HTTPS для ReactJS во время разработки

Я использовал статью с medium, она довольно простая.

GET преобразуется в OPTION

Во время кросс-доменных запросов get-запрос преобразуется в option в том случае, если вы передаёте заголовки и в fetch’е установлено {credentials:»include»}. В таком случае появляется ошибка » 405 method not allowed».

Options-запрос отправляется для того, чтобы браузер понял можно ли вообще передавать заголовки с этого домена и какие.

В том случае если вы передаёте в get-запросе, например, заголовок «Authorization», то должны отдать «пачку» заголовков:

Access-Control-Allow-Credentials: true
// Тут перечисляем наши заголовки
Access-Control-Allow-Headers: Authorization, прочие заголовки запроса... 
// Перечисляем разрешённые методы
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS
// Пишем домен с которого отправляем запрос
Access-Control-Allow-Origin: https://yourdomain.ru

Когда fetch получит эти заголовки, то автоматически отправит уже GET-запрос. Вы можете легко проверить это, если посмотрите, например, в DevTools хрома (вкладка Network)

В том случае если заголовки не будут переданы, то в status code от сервера вы увидите «405 method not allowed». Также вы увидите это, если не перечислите нужный метод в «Access-Control-Allow-Methods»

P.S.

Ссылка на оригинальные примеры https://fetch.spec.whatwg.org/#example-cors-with-credentials

Если вы не понимаете, что такое fetch, предлагаю прочитать мою статью «fetch в reactjs«, возможно также вам понадобиться статья про router в react

Надеюсь я сэкономил вам немного времени, ведь именно это основная цель моего сайта. Если вы хотите и дальше экономить своё время, то обязательно подписывайтесь на обновления сайта с помощью push-уведомлений.