Запись cookie из JavaScript: функция SetCookie и её атрибуты

РазделJavaScript и CSSУровеньНовичок Чтение8 минОпубликовано ТегиJavaScriptCookieASP.NET

Cookie из JavaScript записывается одной строкой, но у этой строки есть формат и набор атрибутов. Собираем аккуратную функцию SetCookie и разбираем, почему cookie иногда «не сохраняется».

Главное. Запись cookie из JavaScript сводится к правильно собранной строке: закодированное значение, срок жизни, путь «/» и флаги secure и samesite. Удалять и перезаписывать нужно с теми же path и domain.

Как устроен document.cookie

Свойство document.cookie выглядит как обычная строка, но работает необычно. При чтении оно возвращает все доступные странице cookie одной строкой через точку с запятой. При записи присваивание не заменяет весь набор, а добавляет или обновляет ровно одну cookie, описанную в правой части.

Строка записи состоит из пары «имя=значение» и необязательных атрибутов: срока жизни, пути, домена и флагов безопасности. Если атрибуты не указаны, получится сессионная cookie, которая живёт до закрытия браузера и видна только страницам из того же каталога, что и текущая. Последнее часто становится сюрпризом: cookie, записанная со страницы /shop/cart.aspx, не видна на /default.aspx.

Функция SetCookie

Ниже вариант, который подходит для большинства задач. Срок жизни задаётся в днях, путь по умолчанию равен корню сайта, чтобы cookie видели все страницы. Значение кодируется через encodeURIComponent: точка с запятой, запятая или пробел внутри значения иначе обрежут его или испортят соседние атрибуты.

Дату окончания формируем через toUTCString. Метод toGMTString, который встречается в старых примерах, давно объявлен устаревшим, хотя возвращает строку того же формата.

cookies.jsJavaScript
// Записывает cookie. days: срок жизни в днях, без него cookie сессионная
function SetCookie(name, value, days, path) {
  var parts = [encodeURIComponent(name) + '=' + encodeURIComponent(value)];
  if (days) {
    var exp = new Date();
    exp.setTime(exp.getTime() + days * 24 * 60 * 60 * 1000);
    parts.push('expires=' + exp.toUTCString());
  }
  parts.push('path=' + (path || '/'));
  if (location.protocol === 'https:') parts.push('secure');
  parts.push('samesite=lax');
  document.cookie = parts.join('; ');
}

// SetCookie('theme', 'blue', 30);

Атрибуты и что они меняют

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

  • expires или max-age: срок жизни. max-age задаётся в секундах и удобнее, но Internet Explorer до девятой версии его не понимал, поэтому в legacy-коде встречается только expires;
  • path: префикс URL, для которого cookie видна. Значение «/» делает её общей для сайта;
  • domain: позволяет поделиться cookie с поддоменами, например domain=example.com для www и shop. Чужой домен указать нельзя, браузер такую запись проигнорирует;
  • secure: cookie уходит только по HTTPS;
  • samesite: правило отправки при переходах с других сайтов. Lax подходит для настроек интерфейса, None требует обязательного secure.
Cookie с одинаковым именем, но разными path или domain, считаются разными. Удаление или перезапись должны идти с теми же атрибутами, иначе появится вторая копия.

Чтение и удаление в пару к записи

Отдельного API для чтения нет: строку document.cookie приходится разбирать самостоятельно. Делим её по точке с запятой, у каждой части отрезаем пробелы и сравниваем имя. Значение декодируем той же парой функций, которой кодировали.

Удалить cookie можно только одним способом: записать её заново с датой в прошлом. Путь и домен при этом должны совпадать с исходными.

cookies.jsJavaScript
function ReadCookie(name) {
  var key = encodeURIComponent(name) + '=';
  var items = document.cookie.split(';');
  for (var i = 0; i < items.length; i++) {
    var c = items[i].replace(/^\s+/, '');
    if (c.indexOf(key) === 0) {
      return decodeURIComponent(c.substring(key.length));
    }
  }
  return null; // не найдена
}

function DeleteCookie(name, path) {
  document.cookie = encodeURIComponent(name) +
    '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=' + (path || '/');
}

Ограничения и безопасность

Cookie отправляются серверу с каждым запросом к домену, включая картинки и стили. Поэтому хранить в них большие объёмы данных невыгодно: каждая лишняя сотня байт умножается на число запросов. Браузеры ограничивают размер одной cookie примерно четырьмя килобайтами, а количество на домен несколькими десятками.

Скрипт видит не все cookie. Если сервер выставил флаг HttpOnly, как это делает ASP.NET для cookie сессии и аутентификации форм, document.cookie её не покажет. Это защита от кражи сессии через XSS, а не ошибка.

  • не храните в cookie пароли, номера карт и персональные данные;
  • значение, записанное в браузере, может подменить пользователь, поэтому на сервере его нужно проверять;
  • для настроек интерфейса (тема, свёрнутая панель, язык) cookie подходят хорошо.

Связка с ASP.NET

Cookie, записанную скриптом, сервер читает обычным образом: Request.Cookies(«theme»).Value в VB.NET. Значение придёт в том виде, в каком его закодировал encodeURIComponent, поэтому на сервере его стоит пропустить через HttpUtility.UrlDecode. Обратное тоже работает: cookie, выставленную через Response.Cookies без HttpOnly, можно прочитать функцией ReadCookie.

Частая причина расхождений: сервер ставит cookie с путём приложения, например /app, а скрипт пишет с путём «/». В итоге у браузера две cookie с одним именем, и какая из них придёт первой, зависит от длины пути.

Как это делают сегодня

Для данных, которые не нужны серверу, сегодня используют localStorage и sessionStorage: они не уходят с каждым запросом и вмещают мегабайты. В Chromium-браузерах появился асинхронный Cookie Store API с методами cookieStore.set и get. Браузеры по умолчанию считают cookie SameSite=Lax, а cookie без Secure для межсайтовых сценариев блокируют.

Вопросы и ответы

Почему cookie не видна на другой странице сайта?

Вероятно, она записана без атрибута path и привязалась к каталогу текущей страницы. Указывайте path=/ при записи, и cookie станет доступна всему сайту.

Почему скрипт не видит cookie сессии ASP.NET?

Сервер выставляет её с флагом HttpOnly, и браузер скрывает такие cookie от JavaScript. Это защитная мера, обходить её не нужно.

Можно ли хранить в cookie объект?

Можно сериализовать его через JSON.stringify и закодировать encodeURIComponent, но размер ограничен примерно четырьмя килобайтами. Для крупных данных лучше localStorage.

Читайте также