HTTP cookies на практике

Узнайте, как работают файлы cookies (куки) HTTP: простые практические примеры с использованием JavaScript и Python.

Что такое файлы cookies в веб-разработке?

Файлы куки — это крошечные фрагменты данных, которые серверная часть может хранить в браузерах пользователя. Отслеживание пользователей, персонализация и, самое главное, аутентификация — наиболее распространенные варианты использования файлов куки.

Они имеют большое количество проблем с конфиденциальностью. Поэтому на протяжении многих лет их использование строго регулируется.

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

Содержание

Настройка бэкенда

Примеры для бэкенда сделаны при помощи Python и его фреймворка Flask. Если вы хотите воспроизводить код из данной статьи, создайте новую виртуальную среду Python и установите Flask.

mkdir cookies && cd $_

python3 -m venv venv
source venv/bin/activate

pip install Flask

В каталоге проекта создайте файл flask_app.py и используйте наши примеры для собственных экспериментов.

Кто создает cookies?

Перво-наперво надо выяснить, откуда берутся куки и кто их создает.

Хотя с помощью свойства document.cookie можно создавать куки непосредственно в самом браузере, в большинстве случаев за их установку отвечает серверная часть (бэкенд).

Под этим мы подразумеваем, что куки создаются:

  • реальным кодом приложения, которое работает на сервере и может быть написано на языках Python, JavaScript, PHP, Java и др.
  • самим веб-сервером, отвечающим на запросы (Nginx, Apache)

Для этого бэкенд отправляет в ответ на запрос HTTP-заголовок с именем Set-Cookie и со строкой, состоящей из пары ключ=значение и необязательных атрибутов:

Set-Cookie: myfirstcookie=somecookievalue

Когда и где создавать эти файлы куки, зависит от соответствующих требований.

Итак, куки это просто строки. Рассмотрим пример с Python и Flask. Сохраните следующий код в файл flask_app.py, который вы создали в директории проекта.

from flask import Flask, make_response

app = Flask(__name__)


@app.route("/index/", methods=["GET"])
def index():
    response = make_response("Here, take some cookie!")
    response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"
    return response

Затем запустите приложение:

FLASK_ENV=development FLASK_APP=flask_app.py flask run

Когда это приложение запущено и пользователь посещает страницу http://127.0.0.1:5000/index/, бэкенд устанавливает заголовок ответа с именем Set-Cookie и с соответствующей парой ключ=значение.

(127.0.0.1:5000 — адрес и порт, которые прослушиваются по умолчанию для Flask-приложений в разработке).

Заголовок Set-Cookie имеет ключевое значение для понимания того, как создавать куки.

response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"

Справа от знака равенства вы видите собственно куки — "myfirstcookie=somecookievalue".

Большинство фреймворков имеют собственные служебные функции для программной установки файлов куки. Во Flask, например, эта функция называется set_cookie().

Под капотом такие функции в ответ на запрос просто устанавливают заголовок с Set-Cookie.

Как можно посмотреть файлы cookies

Снова рассмотрим предыдущий пример с Flask. Когда вы посещаете http://127.0.0.1:5000/index/, серверная часть устанавливает куки в браузере. Чтобы увидеть этот файл куки, вы можете вызвать document.cookie из консоли браузера:

Или проверьте вкладку Storage в инструментах разработчика. Нажмите на Cookies, и вы должны увидеть файл куки:

Также можно использовать команду curl в командной строке. Вы увидите, какие файлы куки были установлены бэкендом.

curl -I http://127.0.0.1:5000/index/

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

curl -I http://127.0.0.1:5000/index/ --cookie-jar mycookies

Для того чтобы вывести куки в консоль, наберите вот эту команду:

curl -I http://127.0.0.1:5000/index/ --cookie-jar -

Обратите внимание, что файлы куки без атрибута HttpOnly доступны в document.cookie из JavaScript в браузере. А вот куки, помеченные как HttpOnly, недоступны из JavaScript.

Чтобы пометить cookie как HttpOnly, нужно передать следующий атрибут:

Set-Cookie: myfirstcookie=somecookievalue; HttpOnly

Теперь куки по-прежнему будут отображаться на вкладке Cookie Storage, но document.cookie вернет пустую строку.

С этого момента для удобства мы будем создавать куки при помощи функции response.set_cookie() из фреймворка Flask.

Проверять файлы куки мы будем тремя способами:

  • командой curl
  • при помощи инструментов разработчика (developer tools) браузера Firefox
  • при помощи инструментов разработчика (developer tools) браузера Chrome.
[python_ad_block]

Итак, у нас есть cookies, и что дальше?

Ваш браузер получил куки. И что теперь? Если у вас есть куки, браузер может отправлять их обратно на серверную часть.

Это может делаться с разными целями. Например, для отслеживания пользователей, персонализации и, самое главное, аутентификации.

При авторизации на сайте сервер может предоставить вам вот такой кук:

Set-Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r

Чтобы правильно идентифицировать вас при каждом последующем запросе, бэкенд проверяет кук, поступающий из браузера в запросе.

Чтобы отправить такой кук, браузер добавляет в запрос заголовок Cookie:

Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r

Как, когда и почему браузер отправляет куки — это темы следующих разделов.

Куки имеют срок давности.  Атрибуты Max-Age и expires

По умолчанию срок действия куков истекает, когда пользователь завершает сеанс, то есть когда он закрывает браузер. Чтобы сохранить куки, мы можем передать атрибуты expires или Max-Age:

Set-Cookie: myfirstcookie=somecookievalue; expires=Tue, 09 Jun 2020 15:46:52 GMT; Max-Age=1209600

Когда присутствуют оба атрибута, Max-Age имеет приоритет над expires.

Cookies, ограниченные атрибутом Path

Рассмотрим бэкенд, который устанавливает новый кук для своего фронтенда при посещении страницы http://127.0.0.1:5000/. При двух других запросах вместо этого мы просто выводим в консоль куки самого запроса:

from flask import Flask, make_response, request

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d", path="/about/")
    return response


@app.route("/about/", methods=["GET"])
def about():
    print(request.cookies)
    return "Hello world!"


@app.route("/contact/", methods=["GET"])
def contact():
    print(request.cookies)
    return "Hello world!"

Запустим наше приложение:

FLASK_ENV=development FLASK_APP=flask_app.py flask run

В другом терминале, установив соединение с корневым маршрутом (root), мы увидим вот такой кук в Set-Cookie:

curl -I http://127.0.0.1:5000/ --cookie-jar cookies

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 23
Set-Cookie: id=3db4adj3d; Path=/about/
Server: Werkzeug/1.0.1 Python/3.8.3
Date: Wed, 27 May 2020 09:21:37 GMT

Обратите внимание, что у кука есть атрибут Path:

Set-Cookie: id=3db4adj3d; Path=/about/

Теперь давайте посетим маршрут /about/, отправив кук, который мы сохранили при первом посещении:

curl -I http://127.0.0.1:5000/about/ --cookie cookies

В терминале, где запущено приложение Flask, вы должны увидеть следующее:

ImmutableMultiDict([('id', '3db4adj3d')])
127.0.0.1 - - [27/May/2020 11:27:55] "HEAD /about/ HTTP/1.1" 200 -

Как и ожидалось, кук возвращается на серверную часть. Теперь попробуйте посетить страницу /contact/:

curl -I http://127.0.0.1:5000/contact/ --cookie cookies

На этот раз в терминале, где запущено приложение Flask, вы должны увидеть вот что:

ImmutableMultiDict([])
127.0.0.1 - - [27/May/2020 11:29:00] "HEAD /contact/ HTTP/1.1" 200 -

Что все это означает? Куки ограничены своими путями. Кук с заданным атрибутом Path не может быть отправлен на другой, несвязанный путь, даже если оба пути находятся в одном домене.

Это первый уровень разрешений для куков.

Если при создании кука атрибут Path задан не был, браузеры по умолчанию принимают его равным ‘/’.

Cookies, ограниченные атрибутом Domain

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

Давайте рассмотрим несколько примеров.

ПРИМЕЧАНИЕ: следующий URL-адрес находится на бесплатном сервере Heroku. Поэтому дайте ему немного времени, чтобы он набрал обороты и после этого откройте консоль браузера.

Несоответствующий хост (неправильный адрес хоста)

Рассмотрим следующий набор куков, установленных для https://serene-bastion-01422.herokuapp.com/get-wrong-domain-cookie/:

Set-Cookie: coookiename=wr0ng-d0m41n-c00k13; Domain=api.valentinog.com

Здесь кук исходит из serene-bastion-01422.herokuapp.com, но в атрибуте Domain значится api.valentinog.com.

У браузеров нет другого выбора, кроме как отклонить данный кук. Chrome даже выдаст предупреждение (в отличии от Firefox):

Несоответствующий хост (поддомен)

Рассмотрим следующий кук, установленный https://serene-bastion-01422.herokuapp.com/get-wrong-subdomain-cookie/:

Set-Cookie: coookiename=wr0ng-subd0m41n-c00k13; Domain=secure-brushlands-44802.herokuapp.com

Здесь кук исходит из serene-bastion-01422.herokuapp.com, но атрибут Domain имеет значение secure-brushlands-44802.herokuapp.com.

Они находятся в одном домене, но поддомены отличаются. И снова браузер отклоняет такой кук:

Соответствующий хост (весь домен)

Теперь рассмотрим следующий кук, установленный при посещении страницы https://www.valentinog.com/get-domain-cookie.html:

set-cookie: cookiename=d0m41n-c00k13; Domain=valentinog.com

Этот кук устанавливается на уровне веб-сервера при помощи Nginx add_header:

add_header Set-Cookie "cookiename=d0m41n-c00k13; Domain=valentinog.com";

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

Здесь браузер с радостью примет кук, потому что хост в Domain включает в себя хост, с которого пришел сам кук.

Другими словами, valentinog.com включает поддомен www.valentinog.com.

Кроме того, кук возвращается при любых новых запросах к valentinog.com, а также при любых запросах к поддоменам на valentinog.com.

Вот запрос к поддомену с прикрепленным куком:

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

Куки и список общедоступных суффиксов

Теперь рассмотрим следующий кук, установленный https://serene-bastion-01422.herokuapp.com/get-domain-cookie/:

Set-Cookie: coookiename=d0m41n-c00k13; Domain=herokuapp.com

Здесь кук поступает с serene-bastion-01422.herokuapp.com, а атрибут Domain имеет значение herokuapp.com. Что в такой ситуации нужно делать браузеру?

Вы можете подумать, что, так как serene-bastion-01422.herokuapp.com входит в домен herokuapp.com, браузер должен принять этот кук.

Но вместо этого он отклонит данный кук, поскольку тот поступает из домена, включенного в список общедоступных суффиксов.

Public Suffix List поддерживается Мозиллой и используется всеми браузерами для ограничения установки куков от имени других доменов.

Полезные ресурсы:

Соответствующий хост (поддомен)

Теперь рассмотрим следующий кук, установленный https://serene-bastion-01422.herokuapp.com/get-subdomain-cookie/:

Set-Cookie: coookiename=subd0m41n-c00k13

Если при создании кука домен не указан, браузеры по умолчанию используют исходный хост в адресной строке. В этом случае наш код будет иметь следующий вид:

response.set_cookie(key="coookiename", value="subd0m41n-c00k13")

Когда кук попадает в хранилище браузера для куков, мы видим, что домен был указан:

Итак, у нас есть этот кук с сайта serene-bastion-01422.herokuapp.com. Куда теперь следует его отправить?

Если вы посетите страницу https://serene-bastion-01422.herokuapp.com/, кук будет сопровождаться следующим запросом:

Но если вы просто зайдете на сайт herokuapp.com, данный кук вообще не покинет ваш браузер:

(То, что herokuapp.com перенаправляется на heroku.com, не играет никакой роли).

Напомним, как браузер решает, что делать с куками. Здесь под хостом отправителя мы подразумеваем фактический URL-адрес, который вы посещаете.

Итак, браузер:

  • Полностью отклоняет кук, если домен или поддомен в атрибуте Domain не соответствуют хосту отправителя.
  • Отклоняет кук, если значение домена включено в список общедоступных суффиксов.
  • Принимает кук, если домен или поддомен в атрибуте Domain совпадает с хостом отправителя.

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

Отправьте мне кук, если хост запроса точно соответствует значению, которое я видел в атрибуте Domain.

Отправьте мне кук, если хост запроса является субдоменом, точно соответствующим значению, которое я видел в атрибуте Domain.

Отправьте мне кук, если хост запроса является субдоменом, например sub.example.dev, включенным в атрибут Domain, например example.dev.

Не отправляйте мне кук, если хост запроса является основным доменом, например example.dev, а атрибут Domain был sub.example.dev.

Вывод: атрибут Domain — это второй уровень разрешений для куков, наряду с атрибутом Path.

Перемещение cookies по запросам AJAX

Куки могут перемещаться по запросам AJAX. Запросы AJAX — это асинхронные HTTP-запросы, сделанные с помощью JavaScript (XMLHttpRequest или Fetch) для получения и отправки данных на серверную часть.

Рассмотрим еще один пример с Flask. У нас есть шаблон, который загружает файл JavaScript. Вот приложение Flask:

from flask import Flask, make_response, render_template

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response

Вот шаблон из файла templates/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="{{ url_for('static', filename='index.js') }}"></script>
</html>

Вот JavaScript-код из файла static/index.js:

const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie();
});

function getACookie() {
  fetch("/get-cookie/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.text();
    })
    .then(text => console.log(text));
}

При посещении страницы http://127.0.0.1:5000/ мы видим кнопку. Нажимая на кнопку, мы делаем Fetch запрос в /get-cookie/, чтобы получить кук обратно. Как и ожидалось, кук попадает в хранилище куков нашего браузера.

Теперь давайте немного изменим наше приложение Flask:

from flask import Flask, make_response, request, render_template, jsonify

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response


@app.route("/api/cities/", methods=["GET"])
def cities():
    if request.cookies["id"] == "3db4adj3d":
        cities = [{"name": "Rome", "id": 1}, {"name": "Siena", "id": 2}]
        return jsonify(cities)
    return jsonify(msg="Ops!")

Кроме того, давайте немного подстроим наш код JavaScript, чтобы сделать еще один Fetch-запрос после получения кука:

const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("/get-cookie/").then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("/api/cities/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}

При посещении страницы http://127.0.0.1:5000/ мы видим кнопку. Нажимая на кнопку, мы делаем Fetch-запрос в /get-cookie/, чтобы получить кук обратно. Как только приходит кук, мы делаем еще один Fetch-. запрос в /api/cities/.

В консоли браузера вы должны увидеть массив городов. Кроме того, на вкладке Network инструментов разработчика вы должны увидеть заголовок с именем кука, передаваемого в бэкэнд по запросу AJAX.

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

Это потому, что по умолчанию Fetch отправляет учетные данные, то есть куки, только тогда, когда запрос попадает в тот же источник, из которого он запускается.

Здесь JavaScript обслуживается шаблоном Flask на странице http://127.0.0.1:5000/.

А теперь давайте посмотрим, что происходит при разных источниках.

Cookies не всегда могут передаваться по запросам AJAX

Рассмотрим другую ситуацию, когда серверная часть работает автономно. Итак, у вас запущено следующее приложение Flask:

FLASK_ENV=development FLASK_APP=flask_app.py flask run

Теперь в другой папке, вне приложения Flask, создайте файл index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="index.js"></script>
</html>

Создайте в той же папке файл JavaScript с именем index.js со следующим кодом:

const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("http://localhost:5000/get-cookie/").then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("http://localhost:5000/api/cities/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}

В этой же папке из терминала запустите команду npx serve.

Эта команда дает вам локальный адрес / порт для подключения, например http://localhost: 42091/. Зайдите на страницу и попробуйте нажать кнопку с открытой консолью браузера (в инструментах разработчика). В консоли вы должны увидеть следующее:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/get-cookie/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)

Это потому, что http://localhost: 5000/ не то же самое, что http://localhost: 42091/. Они имеют разные источники, отсюда и применение CORS.

Источник состоит из схемы, домена и номера порта. Это означает, что http://localhost: 5000/ отличается от http://localhost: 42091/.

Работа с CORS

CORS — это аббревиатура от Cross-Origin Resource Sharing — «Совместное использование ресурсов между разными источниками». Это способ для серверов контролировать доступ к ресурсам в данном источнике, когда код JavaScript, работающий в другом источнике, запрашивает эти ресурсы.

По умолчанию браузеры блокируют запросы AJAX к удаленным ресурсам, которые не находятся в одном источнике, если только сервер не предоставляет определенный HTTP-заголовок с именем Access-Control-Allow-Origin.

Чтобы исправить предыдущую ошибку, нам нужно настроить CORS для Flask:

pip install flask-cors

Далее применим CORS к нашему коду:

from flask import Flask, make_response, request, render_template, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app=app)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response


@app.route("/api/cities/", methods=["GET"])
def cities():
    if request.cookies["id"] == "3db4adj3d":
        cities = [{"name": "Rome", "id": 1}, {"name": "Siena", "id": 2}]
        return jsonify(cities)
    return jsonify(msg="Ops!")

Теперь попробуйте еще раз нажать кнопку с открытой консолью браузера. В консоли вы должны увидеть следующее:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/api/cities/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)

Несмотря на то, что мы получили ту же ошибку, на этот раз виноват второй маршрут.

К запросу не прикреплен кук с именем «id», поэтому Flask аварийно завершает работу и не устанавливает Access-Control-Allow-Origin.

В этом можно убедиться, посмотрев запрос во вкладке Network (в инструментах разработчика). Такие куки не отправляются:

Чтобы включить cookies в Fetch-запросы из разных источников, мы должны предоставить флаг учетных данных.

Без этого флага Fetch просто игнорирует куки. Чтобы исправить наш пример, изменим код следующим образом:

const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("http://localhost:5000/get-cookie/", {
    credentials: "include"
  }).then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("http://localhost:5000/api/cities/", {
    credentials: "include"
  })
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}

credentials: "include" должен присутствовать в первом Fetch-запросе, чтобы сохранить кук в хранилище.

fetch("http://localhost:5000/get-cookie/", {
    credentials: "include"
  })

Он также должен присутствовать во втором запросе, чтобы разрешить передачу куков обратно в серверную часть:

  fetch("http://localhost:5000/api/cities/", {
    credentials: "include"
  })

Попробуйте еще раз, и вы увидите, что нам нужно исправить еще одну ошибку на сервере:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/get-cookie/. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).

Чтобы разрешить передачу куков в запросах CORS, серверная часть также должна предоставить заголовок Access-Control-Allow-Credentials. Это легко поправить.

CORS(app=app, supports_credentials=True)

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

Выводы: чтобы cookies передавались по запросам AJAX между разными источниками, вы должны обеспечить следующее:

  • credentials: "include" во фронтенде для Fetch-запроса
  • Access-Control-Allow-Credentials и Access-Control-Allow-Origin в бэкенде.

Куки могут передаваться по запросам AJAX, но они должны соответствовать доменным правилам, которые мы здесь описали.

Конкретный пример

В нашем предыдущем примере использовался localhost, чтобы все было просто и воспроизводимо на вашем локальном компьютере.

Чтобы представить обмен куками по запросам AJAX в реальном мире, вы можете наметить себе следующий сценарий:

  1. Пользователь заходит на сайт https://www.a-example.dev.
  2. Он нажимает кнопку или выполняет какое-либо действие, которое запускает Fetch-запрос на https://api.b-example.dev.
  3. https://api.b-example.dev устанавливает кук с атрибутом Domain=api.b-example.dev.
  4. При последующих Fetch запросах на https://api.b-example.dev кук отправляется обратно.

Куки могут иметь своего рода секрет: атрибут Secure

Но этот секрет не такой уж и секретный, по большому счету.

Атрибут Secure для куков гарантирует, что куки никогда не будут приняты через HTTP. То есть браузер отклоняет куки с данным атрибутом, если соединение не происходит через HTTPS.

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

Set-Cookie: "id=3db4adj3d; Secure"

В коде сервера Flask:

response.set_cookie(key="id", value="3db4adj3d", secure=True)

Если вы хотите попробовать это в реальной среде, выполните следующую команду в консоли и обратите внимание, как здесь curl не сохраняет куки через HTTP:

curl -I http://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -

Примечание: это будет работать только в версии curl >=7.64.0, который реализует rfc6265bis. Более старые версии curl реализуют RCF6265.

А через HTTPS куки появляются в хранилище:

curl -I https://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -

Вот как оно выглядит:

serene-bastion-01422.herokuapp.com      FALSE   /       TRUE    0       id      3db4adj3d

Чтобы проверить куки в браузере, посетите обе версии указанного выше URL-адреса и проверьте хранилище куков в инструментах разработчика.

Не обманывайтесь самим словом Secure (безопасный). Куки придут к вам по безопасному соединению, но после того как они окажутся в вашем браузере, никакой защиты у них не будет.

По этой причине безопасный кук, как и любой другой, не предназначен для передачи конфиденциальных данных, даже если название предполагает обратное.

Не трогайте мои куки: атрибут HttpOnly

Атрибут HttpOnly для кука гарантирует, что этот самый кук недоступен для кода JavaScript. Это самая главная форма защиты против XSS атак.

Однако он отправляется при каждом последующем HTTP-запросе с учетом всех разрешений, установленных атрибутами Domain и Path.

Чтобы пометить кук как HttpOnly, установите данный атрибут следующим образом:

Set-Cookie: "id=3db4adj3d; HttpOnly"

В сервере Flask:

response.set_cookie(key="id", value="3db4adj3d", httponly=True)

К куку, помеченному как HttpOnly, нельзя получить доступ из JavaScript. При проверке в консоли document.cookie будет возвращать пустую строку.

Однако, если установить credentials обратно в состояние include, то при помощи Fetch-запроса можно будет опять отправлять и получать куки с атрибутом HttpOnly. Разумеется, при наличии разрешений от атрибутов Path и Domain.

fetch(/* url */, {
  credentials: "include"
})

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

Зловещий атрибут SameSite

Собственные и сторонние куки

Рассмотрим кук, полученный при посещении страницы https://serene-bastion-01422.herokuapp.com/get-cookie/:

Set-Cookie: simplecookiename=c00l-c00k13; Path=/

Мы называем этот вид куков собственными. То есть, если я посещаю этот URL-адрес в браузере, и если я посещаю тот же URL-адрес или другую страницу на этом сайте (при условии, что атрибут Path установлен в значение /), то браузер отправляет куки обратно на сайт. Обычные куки, одним словом.

Теперь рассмотрим другую веб-страницу по адресу https://serene-bastion-01422.herokuapp.com/get-frog/. Эта страница также устанавливает куки и, кроме того, загружает изображение со стороннего ресурса, размещенное по адресу https://www.valentinog.com/cookie-frog.jpg.

Этот сторонний ресурс, в свою очередь, сам устанавливает свои куки. Вы можете увидеть этот сценарий на скрине ниже:

Примечание: если вы используете Chrome 85, вы не увидите этот кук. Начиная с этой версии Chrome отклоняет его. Мы называем этот вид куков сторонними. Другой пример стороннего кука:

  1. Пользователь посещает страницу https://www.a-example.dev.
  2. Он нажимает кнопку или выполняет какое-либо действие, которое запускает Fetch-запрос на странице https://api.b-example.dev
  3. Теперь на странице https://www.a-example.dev хранится сторонний кук с https://api.b-example.dev.

Работа с атрибутом Samesite

На момент написания данной статьи сторонние куки вызывают всплывающее окно с предупреждением в консоли Chrome:

«Файл cookie, связанный с межсайтовым ресурсом http://www.valentinog.com/, был установлен без атрибута SameSite . Будущая версия Chrome будет доставлять файлы cookie с межсайтовыми запросами только в том случае, если они установлены с помощью SameSite=None и Secure .»

Браузер пытается нам сказать, что сторонние куки должны иметь новый атрибут SameSite. Но почему?

Атрибут SameSite — это новая функция, направленная на повышение безопасности куков. Она предназначена, чтобы предотвратить подделку межсайтовых запросов и избежать утечек конфиденциальности.

Атрибуту SameSite может быть присвоено одно из трех значений:

  • Strict
  • Lax
  • None

Если мы являемся службой, предоставляющей встраиваемые виджеты (iframe), или нам необходимо размещать куки на удаленных веб-сайтах (по уважительной причине, а не для дикого отслеживания), эти куки должны быть помечены как SameSite=None и Secure:

Set-Cookie: frogcookie=fr0g-c00k13; SameSite=None; Secure

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

«Файл cookie, связанный с межсайтовым ресурсом http://www.valentinog.com/, был установлен без атрибута SameSite . Он был заблокирован, так как Chrome теперь доставляет файлы cookie только с межсайтовыми запросами, если для них установлено значение SameSite=None и Secure

Другими словами SameSite=None; Secure заставит сторонние куки работать так, как они работают сегодня, с той лишь разницей, что они должны передаваться только через HTTPS.

Настроенный таким образом кук отправляется вместе с каждым запросом, если домен и путь совпадают. Это нормальное поведение.

Стоит отметить, что атрибут SameSite относится не только к сторонним кукам.

По умолчанию браузеры будут применять SameSite=Lax для всех куков, как собственных, так и сторонних, если данный атрибут отсутствует. Вот какая политику у браузера Firefox Nightly относительно собственных куков:

«Кук get_frog_simplecookiename имеет атрибут SameSite, установленный в значение lax, потому что в нем отсутствовал атрибут SameSite, а SameSite=lax является значением по умолчанию для этого атрибута.»

Сторонние куки с атрибутом SameSite=Strict будут полностью отклоняться браузером.

Напомним, вот поведение браузера для разных значений SameSite:

ЗначениеВходящий кукВыходящий кук
StrictОтклонить
LaxПринятьОтправить безопасным методом HTTP
None + SecureПринятьОтправить

Чтобы узнать больше о SameSite и подробно разобраться во всех вариантах использования этого атрибута, почитайте эти отличные ресурсы:

Куки и аутентификация

Аутентификация — одна из самых сложных задач в веб-разработке. Похоже, что вокруг этой темы так много путаницы, поскольку аутентификация на основе токенов с JWT, видимо, заменяет «старые» твердые шаблоны, такие как аутентификация на основе сессий.

Посмотрим, какую роль здесь играют куки.

Аутентификация на основе сессий

Аутентификация — один из наиболее распространенных случаев использования куков.

Когда вы посещаете сайт, запрашивающий аутентификацию, то при отправке учетных данных (например, через форму) бэкенд отправляет фронтенду заголовок Set-Cookie.

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

Set-Cookie: sessionid=sty1z3kz11mpqxjv648mqwlx4ginpt6c; expires=Tue, 09 Jun 2020 15:46:52 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax

В этом заголовке Set-Cookie сервер может включать кук с именем session, session id или аналогичный.

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

На этом этапе, чтобы правильно идентифицировать пользователя, серверная часть связывает идентификатор сессии с самим сеансом, который находится в хранилище за кулисами.

Аутентификация на основе сессий представляет из себя отслеживание состояний, поскольку серверная часть должна отслеживать сессии для каждого пользователя. Хранилищем для этих сессий может быть:

  • база данных
  • хранилище ключей / значений, такое как Redis
  • файловая система.

Из этих трех хранилищ сессий предпочтение следует отдавать Redis (или ему подобным).

Обратите внимание, что аутентификация на основе сессий не имеет ничего общего с хранилищем сессий браузера.

Она так называется (аутентификацией на основе сессий) только потому, что соответствующие данные для идентификации пользователя находятся в хранилище сессий бэкенда, что не то же самое, что хранилище сессий браузера.

Когда использовать аутентификацию на основе сессий?

Используйте ее всегда, когда это возможно. Аутентификация на основе сессий — одна из самых простых, безопасных и понятных форм аутентификации для сайтов. Она доступна по умолчанию во всех самых популярных веб-фреймворках, например в Django.

Но ее природа, связанная с отслеживанием состояния, также является и ее основным недостатком, особенно когда сайт обслуживается балансировщиком нагрузки. В этом случае могут помочь такие методы, как закрепление сессий или хранение сессий в централизованном хранилище Redis.

Замечание по поводу JWT

JWT (сокращение от JSON Web Tokens) — это механизм аутентификации, набирающий популярность в последние годы.

JWT хорошо подходит для одностраничных и мобильных приложений, но также создает и новый набор проблем. Типичный процесс для внешнего приложения, желающего пройти аутентификацию по API, следующий:

  1. Фронтенд отправляет учетные данные бэкенду.
  2. Бэкенд проверяет учетные данные и отправляет обратно токен.
  3. Фронтенд отправляет токен при каждом последующем запросе.

Главный вопрос, который возникает при таком подходе: где нам хранить этот токен во фронтенде, чтобы пользователь оставался авторизованным?

Самым естественным поступком для того, кто пишет на JavaScript, является сохранение токена в localStorage. Это плохо по многим причинам.

localStorage легко доступен из кода JavaScript и является легкой мишенью для XSS-атак.

Чтобы решить эту проблему, большинство разработчиков прибегают к сохранению токена JWT в куке, полагая, что HttpOnly и Secure могут защитить кук, по крайней мере, от XSS-атак.

Новый атрибут SameSite, установленный в состояние SameSite=Strict, также защитит ваш «кукинизированный» JWT от атак CSRF (Межсайтовая подделка запроса). Но это также полностью аннулирует вариант использования JWT во многих случаях, потому что SameSite = Strict не отправляет куки по запросам из разных источников!

Как насчет того, чтобы установить атрибут в состояние SameSite=Lax? Этот режим позволяет отправлять куки с помощью безопасных методов HTTP, а именно GET, HEAD, OPTIONS и TRACE. Запросы POST ни в коем случае не будут передавать куки.

На самом деле хранение токена JWT в куке или в localStorage — плохая идея.

Если вы действительно хотите использовать JWT вместо аутентификации на основе сессий, вы можете использовать JWT с обновляемыми токенами, чтобы пользователь все время оставался в системе.

Заключение

HTTP-куки существуют с 1994 года. Они просто повсюду.

Куки представляют собой простые текстовые строки, но их можно точно настроить для различных уровней допуска с помощью атрибутов Domain и Path. Их можно передавать только по HTTPS с помощью атрибута Secure и скрыть от JavaScript при помощи атрибута HttpOnly.

Кук может использоваться для персонализации взаимодействия с пользователем, аутентификации пользователей или для скрытых целей, таких как отслеживание.

Но при любом предполагаемом использовании куки могут открывать пользователей для атак и уязвимостей.

Так что же может сделать куки безопасными? Это просто невозможно. Мы можем считать их относительно безопасными, если они:

  • передаются только через HTTPS, имеют атрибут Secure
  • имеют атрибут HttpOnly , если это возможно
  • имеют правильно сконфигурированный атрибут SameSite 
  • не содержат важных данных.

Спасибо за внимание!

Перевод статьи «A practical, Complete Tutorial on HTTP cookies»