Cookie, localStorage и IndexedDB: где хранить состояние
Сравните cookie, localStorage и IndexedDB по области действия, передаче по сети, объёму и вытеснению и выберите, где должно жить каждое состояние.
Нужна структурированная документация по теме Идентичность?
Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.
Что хранит каждый механизм
У страницы есть четыре типичных места для хранения состояния, и они различаются тем, кто может читать данные, когда они передаются по сети и как долго живут. Cookie представляют собой небольшие пары «имя и значение», которые могут задавать и сервер, и страница. У Web Storage две части, localStorage и sessionStorage, и они хранят для скриптов строковые пары «ключ и значение». IndexedDB представляет собой асинхронную транзакционную базу данных для структурированных данных. Выбор между ними определяется жизненным циклом и тем, что доступно извне, а не привычкой.
Cookie описаны в RFC 6265. Сервер задаёт cookie заголовком ответа Set-Cookie, либо скрипт задаёт её через document.cookie, а браузер прикрепляет подходящие cookie к последующим запросам в заголовке Cookie. Атрибуты Domain, Path, Secure и HttpOnly, а также более поздний атрибут SameSite, описанный в MDN, сужают, куда cookie отправляется и кто может её прочитать. Cookie с атрибутом HttpOnly недоступна скрипту страницы, поэтому она подходит для идентификатора сеанса, который выдаёт сервер. Руководство MDN по cookie описывает, как эти атрибуты работают в современных браузерах.
Web Storage описан в стандарте HTML. localStorage и sessionStorage предоставляют один и тот же синхронный интерфейс из getItem, setItem, removeItem и clear со строковыми ключами и строковыми значениями. Всё более сложное, например объект или список, приложение должно сериализовать само. Поскольку вызовы синхронны, большие операции чтения и записи в основном потоке могут задерживать отрисовку, поэтому Web Storage подходит для небольших значений.
IndexedDB описана в спецификации W3C. Она хранит структурированные значения, включая файлы и blob-объекты, в хранилищах объектов, у которых могут быть индексы, а чтение и запись выполняет через асинхронные запросы внутри транзакций. У базы данных есть номер версии, а изменение схемы выполняется на шаге обновления, который контролирует приложение. IndexedDB доступна и в воркерах, поэтому тяжёлое чтение не обязано занимать основной поток. Платой становится больше кода и больше состояний, которые нужно обработать, чем при однострочном вызове localStorage.
Хранилище источника включает и другие компоненты, например API Cache и регистрации сервис-воркеров. Они находятся рядом с этими механизмами и подчиняются тем же правилам квоты и вытеснения в браузерах, которые реализуют Storage Standard. Поэтому очистка или вытеснение может удалить больше, чем сравниваемые здесь хранилища, и проверяющему не следует считать, что значение из одного хранилища переживёт остальные.
Область действия и передача по сети
Больше всего механизмы различаются областью действия. Web Storage и IndexedDB привязаны к источнику (origin), то есть к сочетанию схемы, хоста и порта, поэтому https://example.com и https://example.com:8443 хранят разные данные. Cookie, напротив, привязаны к хосту и пути, и спецификация cookie не разделяет их по портам; единственный элемент контроля, связанный со схемой, это атрибут Secure. SameSite добавляет отдельное понятие сайта, то есть регистрируемого домена, которое определяет, сопровождает ли cookie межсайтовые запросы. Различайте источник и сайт, когда рассуждаете о том, какой код может видеть значение.
Из этого следует и передача по сети. Из четырёх механизмов только cookie прикрепляются к HTTP-запросам автоматически, поэтому каждый подходящий запрос несёт их независимо от того, нужно ли серверу это значение. Это удобно для идентификатора сеанса, который сервер должен читать при каждом запросе, и накладно для крупных данных, потому что эти байты уходят с каждым запросом к хосту. localStorage, sessionStorage и IndexedDB не покидают браузер, пока код приложения не прочитает значение и не отправит его. Эта разница определяет и то, какая сторона может действовать: сервер читает cookie без выполнения какого-либо скрипта, но Web Storage и IndexedDB он не видит вовсе.
Доступность для скриптов устроена зеркально. Любой скрипт, выполняющийся в источнике, может читать localStorage, sessionStorage и IndexedDB этого источника, включая сторонние скрипты, которые подключает страница, и любой скрипт, внедрённый через уязвимость межсайтового выполнения сценариев. Cookie с атрибутом HttpOnly скрыта от скриптов, поэтому она безопаснее как место для учётных данных. Токен доступа (bearer), сохранённый в localStorage, может прочитать тот же код, который отрисовывает страницу. Зафиксируйте это как проектный компромисс; это не причина избегать Web Storage для обычных предпочтений.
Размер cookie заслуживает отдельного замечания. Поскольку cookie передаются вместе с запросами, растущий набор cookie увеличивает каждый запрос, а браузеры вводят собственные ограничения на размер и число cookie для одного хоста. RFC 6265 требует от пользовательских агентов поддерживать лишь скромные минимумы, поэтому cookie должна нести идентификатор или короткий флаг, а более крупные данные лучше оставить серверной записи или клиентскому хранилищу.
Встроенные и сторонние контексты добавляют ещё один слой. Браузеры всё чаще разделяют хранилище и cookie по сайту верхнего уровня, поэтому встроенный фрейм может видеть иное хранилище, чем тот же источник видит как страница верхнего уровня. Подробности зависят от браузера и версии. Статья о разделении хранилища браузера и приватности объясняет, как это проверять. Если функция опирается на состояние внутри встроенного фрейма, проверяйте её именно во встроенном положении и не считайте, что результат для верхнего уровня переносится.
Срок жизни, объём и вытеснение
У срока жизни два конца: момент, когда состояние по замыслу перестаёт быть доступным, и момент, когда его удаляет браузер. Cookie сеанса, то есть без Expires и Max-Age, живут до конца сеанса браузера, границу которого определяет сам браузер, а некоторые браузеры восстанавливают сеансы и сохраняют их после перезапуска. Постоянные cookie живут до даты истечения или пока их не удалит пользователь или браузер. У localStorage и IndexedDB нет собственного срока действия, и данные остаются, пока их не очистит скрипт, пользователь или браузер. sessionStorage живёт столько же, сколько его контекст просмотра верхнего уровня, примерно как вкладка, и переживает перезагрузки, но не закрытие вкладки.
Закрытие разных элементов хорошо показывает разницу. Закрытие вкладки завершает sessionStorage этой вкладки, но оставляет на месте cookie сеанса, localStorage и IndexedDB. Закрытие всего браузера обычно завершает cookie сеанса, хотя восстановление сеанса может их вернуть, и оставляет localStorage и IndexedDB. Вторая вкладка с тем же источником, открытая независимо, использует общие cookie, localStorage и IndexedDB, но получает собственный sessionStorage (окно, открытое скриптом, начинает с копии). Событие storage также уведомляет другие документы того же источника об изменении localStorage, благодаря чему вкладки могут оставаться согласованными.
Объём тоже различается. Cookie ограничены небольшими значениями и ограниченным числом для хоста. Web Storage обычно разрешает несколько мегабайт на источник, а IndexedDB позволяет значительно больше в пределах квоты, которую браузер выводит из общего объёма диска. Эти значения зависят от браузера, и страница MDN о квотах хранилища и критериях вытеснения прямо об этом говорит. Приложению следует узнавать свои лимиты во время работы и обрабатывать ошибку квоты, а не закладывать предположение, взятое из одного браузера.
Storage Standard добавляет самое важное правило для проектирования: сохранность данных по умолчанию обеспечивается по мере возможности. У каждого источника есть контейнер хранилища, и согласно Storage Standard браузер может очистить контейнер в режиме «по мере возможности» (best-effort), когда ему нужно место, удалив данные источника целиком и без обещания спросить заранее. Приложение может вызвать navigator.storage.persist(), чтобы запросить постоянное хранение, а браузер решает, предоставить ли его, по собственной политике, которая может включать запрос к пользователю. navigator.storage.estimate() сообщает приблизительные объём использования и квоту, а navigator.storage.persisted() показывает, является ли контейнер постоянным. Относитесь ко всем трём как к подсказкам, а не к гарантиям.
Вытеснение не единственная причина, по которой состояние исчезает. Пользователи очищают данные сайта, приватные режимы удаляют хранилище при закрытии окна, сайт может отправить заголовок ответа Clear-Site-Data, чтобы попросить браузер очистить cookie или хранилище своего источника, а обновление браузера или смена профиля могут сбросить то, что хранит профиль. Политики меняются и от версии к версии, поэтому однажды замеченное поведение не является обещанием. Спецификации и MDN описывают это поведение как зависящее от браузера, и ничто здесь не обещает одинаковых квот, сроков действия или вытеснения в разных браузерах, версиях и приватных режимах.
Сохранённые данные также переживают код, который их записал. Когда версия меняет форму сохранённого значения, старые записи нужно прочитать, перенести или отбросить, а изменение схемы IndexedDB требует повышения версии и шага обновления. Поле версии внутри значений localStorage служит той же цели. Команда, пропустившая этот шаг, обнаружит это в тот момент, когда вернувшийся браузер загрузит данные, записанные более ранней версией.
Поскольку сохранность обеспечивается лишь по мере возможности, приложению следует считать хранилище браузера кэшем состояния, которое оно может восстановить, если только это не единственная копия и пользователя об этом не предупредили. Если значения нет, приложение должно вернуться к документированному значению по умолчанию, заново получить состояние с сервера или попросить пользователя войти или заново ввести черновик. Оборачивайте чтение и запись в обработку ошибок, потому что запись может вызвать ошибку квоты, а некоторые контексты вообще запрещают хранилище, и убедитесь, что путь первого запуска работает с пустым хранилищем.
Как выбрать механизм для каждого состояния
Отталкивайтесь от состояния, а не от API. Для идентификатора сеанса, который сервер должен читать при каждом запросе, используйте cookie с Secure, HttpOnly и подходящим значением SameSite, а также со сроком жизни, который сервер может принудительно ограничить и отозвать. Этот выбор определяют два компромисса: передача по сети и доступность для скриптов. Автоматическая доставка и защита от чтения скриптами перевешивают ограничение размера, потому что идентификатор крошечный.
Для небольшого предпочтения, такого как тема, язык или закрытое уведомление, обычно хватает localStorage. Значение представляет собой короткую строку, нужно оно только клиентскому коду, а его потеря стоит пользователю одного щелчка. Если сервер должен сформировать первый ответ с учётом этого предпочтения, лучше подойдёт cookie, потому что сервер её видит; в остальных случаях не кладите предпочтения в каждый запрос. Используйте sessionStorage, когда значение должно закончиться вместе с вкладкой, например незавершённый шаг формы, который не должен появляться в другой вкладке.
Для структурированных данных, работающих без соединения, таких как очередь неотправленных правок, кэшированные записи или файлы, используйте IndexedDB. Она справляется с большими объёмами, индексами и транзакциями и работает из воркеров. Её цена в том самом правиле «по мере возможности»: приложение, которое хранит там единственную копию неотправленной правки, полагается на контейнер, который браузер может вытеснить. Помечайте такие данные в интерфейсе как ожидающие, синхронизируйте их с сервером, когда это возможно, и запрашивайте постоянное хранение только тогда, когда данные его оправдывают.
Смешанное решение нормально и часто правильно. Продукт может держать cookie сеанса с атрибутом HttpOnly, предпочтение темы в localStorage и очередь для работы без соединения в IndexedDB, выбирая каждое место по его жизненному циклу. Чего следует избегать, так это дублирования одного значения в нескольких местах без правила о том, какая копия главная, потому что копии расходятся, когда одну вытеснили или очистили, а другую нет. Назначьте одного владельца для каждого состояния и считайте остальные копии производными.
Отсутствующее значение требует внимательного прочтения. Отсутствие cookie или пустой localStorage говорит приложению лишь о том, что этот браузер не сохранил значение или никогда его не получал. Это ничего надёжного не сообщает о том, кто пользователь, первый ли это визит и какой клиент запущен, поэтому из этого не следует делать вывод об идентичности или доверии. Используйте это, чтобы решить, что показать или восстановить, а решения о людях оставляйте аутентификации. Это сравнение предназначено для проектирования и проверки хранилища вашего собственного приложения; чтение, копирование или подмена состояния, сохранённого другим сайтом, выходит за его рамки.
Проверка хранилища в сценарии с несколькими контекстами
Командам, которые выполняют один и тот же сценарий в нескольких контекстах браузера, нужно знать, с каким состоянием начинает каждый контекст. Контексты браузера хранят cookie и данные отдельно, поэтому состояние одного контекста не появляется в другом, и это разделяет учётные записи. Статья об изоляции браузера для нескольких аккаунтов подробнее описывает эту модель изоляции, и приведённые ниже проверки опираются на неё.
Проверяющему полезно задать вопрос: какое состояние существует в начале и что делает сценарий, когда его нет. Cookie остаются единственным слоем, который команда может описать как повторяемое начальное состояние при запуске, о чём рассказывает материал об управлении cookie в браузере для сценариев с несколькими идентичностями. localStorage и IndexedDB в новом контексте обычно начинают пустыми и заполняются по мере работы приложения, поэтому тест, который от них зависит, должен создавать это состояние собственными сценариями приложения и записывать, как это было сделано.
BotBrowser документирует загрузку cookie при запуске с помощью флага --bot-cookies (уровень PRO), включая импорт для отдельного контекста через botbrowserFlags, и документирует, что у каждого BrowserContext есть собственное хранилище, собственные cookie и собственное состояние сеанса, поэтому команда может повторять документированное начальное состояние cookie и держать идентичности раздельными. BotBrowser не документирует предварительную загрузку localStorage или IndexedDB, не изменяет спецификации хранилища браузера и правила квоты и вытеснения и не может гарантировать, что целевой сайт сохранит или примет какое-либо сохранённое состояние. Документация по управлению cookie и документация по изоляции нескольких аккаунтов описывают поддерживаемое поведение.
Фиксируйте итог проверки простыми словами. Хорошая запись называет механизм, его область действия, ожидаемое поведение при истечении или вытеснении и то, что делает приложение при отсутствии состояния, и не содержит пользовательского содержимого и секретных значений. Запись можно повторять после крупного обновления браузера, изменения кода хранения или изменения атрибутов cookie и сравнивать с последним принятым результатом. Сбой должен называть нарушенную границу, например cookie, которая не была отправлена, или контейнер, который очистили, и владельца следующего действия.
Изменения атрибутов cookie заслуживают отдельного повторного прогона, потому что со временем браузеры меняют значения SameSite по умолчанию и обработку сторонних запросов. Сравните сценарий до и после изменения и сохраняйте последнюю принятую конфигурацию, пока новая не пройдёт проверку.
Выполните проверки хранилища
Применяйте эти проверки к каждому состоянию, которое хранит сценарий, и записывайте для каждой результат: пройдена или провалена.
- Для каждого из механизмов cookie, localStorage, sessionStorage и IndexedDB, которые использует сценарий, в записи указано, отправляется ли он с HTTP-запросами. Проверка пройдена, если сетевая панель браузера показывает заголовок Cookie в подходящих запросах и не показывает значений
Web Storageили IndexedDB ни в одном запросе. Проверка провалена, если значение, которое считалось клиентским, появилось в запросе. - В записи названа область действия каждого элемента: источник для
Web Storageи IndexedDB, хост и путь для cookie. Откройте ту же страницу на втором источнике, например на другом порту или поддомене, и убедитесь, что значения localStorage и IndexedDB там не видны. Проверка провалена, если в записи сказано только «сайт» без названия применимой границы. - Закройте вкладку и снова откройте страницу, затем перезапустите браузер и запишите, какие из элементов остаются после каждого шага: cookie сеанса, постоянная cookie, значение localStorage, значение sessionStorage и запись IndexedDB. Отметьте версию браузера, потому что восстановление сеанса и политики различаются. Проверка пройдена, если оставшиеся элементы совпадают со столбцом срока жизни в записи для этой версии браузера; при любом расхождении проверка не пройдена.
- Для идентификатора сеанса, небольшого предпочтения и структурированных данных для работы без соединения в записи названы выбранный механизм и компромисс жизненного цикла, определивший выбор. Проверка провалена, если идентификатор сеанса лежит в читаемом скриптами хранилище без записанной причины.
- Очистите один элемент средствами браузера для данных сайта и перезагрузите страницу. Проверка пройдена, если приложение показывает документированный запасной вариант, то есть значение по умолчанию, повторное получение или приглашение войти, без необработанной ошибки. Проверка провалена, если страница ломается или код воспринимает отсутствие значения как сведения о том, кто пользователь.
- Запишите ответы
navigator.storage.persisted()иnavigator.storage.estimate()как наблюдения. Проверка пройдена, если приложение продолжает работать, когда persisted() возвращает false, а сохранённые данные удалены. Проверка провалена, если приложение предполагает, что постоянное хранение было предоставлено. - Повторите проверки после крупного обновления браузера, изменения кода хранения или изменения атрибутов cookie. Сохраняйте последнюю принятую запись, пока повторный прогон не пройдёт.
Источники
Похожие статьи
Переведите BotBrowser из исследований в продакшн
Используйте эти руководства, чтобы понять модель, а затем перейти к кроссплатформенной валидации, изолированным контекстам и масштабируемому браузерному развертыванию.