ByteGuide
← Все статьи🇷🇺 Русский

Синхронизация данных между поддоменами через Cookie

Как передавать данные между основным доменом и поддоменами с помощью Cookie. Разбираем настройку domain, SameSite и решаем проблемы с localhost.

cookiessubdomainsauthenticationcross-domainbrowser
⏱️ 9 мин. чтения

Синхронизация данных между поддоменами через Cookie

🤔 Проблема

Представьте: у вас есть основной сайт example.com и поддомен app.example.com. Пользователь выбирает тему (светлую/тёмную) на основном сайте, переходит на поддомен — и видит дефолтную тему. Приходится выбирать заново.

Или другой сценарий: пользователь авторизовался на example.com, перешёл на api.example.com — сессия потеряна, нужно логиниться заново.

Вопрос: Как синхронизировать данные между доменом и поддоменами?

🎯 Реальные кейсы

1. Единая тема оформления

example.com          → пользователь выбрал тёмную тему
app.example.com      → должна быть та же тёмная тема
admin.example.com    → тоже тёмная тема

2. Авторизация и сессии

example.com          → пользователь залогинился
api.example.com      → нужен доступ к токену
cdn.example.com      → проверка авторизации для приватных файлов

3. Настройки пользователя

example.com          → язык интерфейса: русский
shop.example.com     → язык должен сохраниться
blog.example.com     → язык тоже русский

По умолчанию cookie доступна только на домене, где была создана:

// На example.com
document.cookie = "theme=dark; path=/"

// Результат:
// example.com     ✅ theme=dark
// app.example.com ❌ нет доступа

С указанием domain cookie доступна на всех поддоменах:

// На example.com
document.cookie = "theme=dark; path=/; domain=.example.com"

// Результат:
// example.com     ✅ theme=dark
// app.example.com ✅ theme=dark
// api.example.com ✅ theme=dark

Важно: точка перед доменом .example.com включает все поддомены.


📝 Практическая реализация

Пример 1: Синхронизация темы

React + Next.js

// lib/useThemeSync.ts
"use client"

import { useEffect } from 'react'
import { useTheme } from 'next-themes'

export function useThemeSync() {
  const { theme, setTheme } = useTheme()

  useEffect(() => {
    // Читаем тему из cookie при загрузке
    const themeCookie = document.cookie
      .split('; ')
      .find(row => row.startsWith('theme='))
      ?.split('=')[1]

    if (themeCookie && themeCookie !== theme) {
      setTheme(themeCookie)
    }
  }, [])

  useEffect(() => {
    // Сохраняем тему в cookie при изменении
    if (theme) {
      const isProduction = window.location.hostname.includes('example.com')
      
      let cookieString = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax`
      
      // На production добавляем domain для всех поддоменов
      if (isProduction) {
        cookieString = `theme=${theme}; path=/; domain=.example.com; max-age=31536000; SameSite=Lax; Secure`
      }

      document.cookie = cookieString
    }
  }, [theme])
}

Vanilla JavaScript

// Функция для сохранения темы
function saveTheme(theme) {
  const isProduction = window.location.hostname.includes('example.com')
  
  let cookie = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax`
  
  if (isProduction) {
    cookie += '; domain=.example.com; Secure'
  }
  
  document.cookie = cookie
}

// Функция для чтения темы
function getTheme() {
  const cookie = document.cookie
    .split('; ')
    .find(row => row.startsWith('theme='))
  
  return cookie ? cookie.split('=')[1] : null
}

// Использование
const savedTheme = getTheme()
if (savedTheme) {
  applyTheme(savedTheme)
}

// При изменении темы
themeToggle.addEventListener('click', () => {
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark'
  saveTheme(newTheme)
  applyTheme(newTheme)
})

Пример 2: Синхронизация языка

// lib/languageSync.ts
export const saveLanguage = (lang: string) => {
  const isProduction = window.location.hostname.includes('example.com')
  
  const cookie = isProduction
    ? `language=${lang}; path=/; domain=.example.com; max-age=31536000; SameSite=Lax; Secure`
    : `language=${lang}; path=/; max-age=31536000; SameSite=Lax`
  
  document.cookie = cookie
}

export const getLanguage = (): string | null => {
  const cookie = document.cookie
    .split('; ')
    .find(row => row.startsWith('language='))
  
  return cookie ? cookie.split('=')[1] : null
}

// Использование в компоненте
const savedLang = getLanguage()
if (savedLang) {
  i18n.changeLanguage(savedLang)
}

Пример 3: Авторизация (токены)

// lib/auth.ts
export const saveAuthToken = (token: string) => {
  const isProduction = window.location.hostname.includes('example.com')
  
  const cookie = isProduction
    ? `auth_token=${token}; path=/; domain=.example.com; max-age=86400; SameSite=Strict; Secure; HttpOnly`
    : `auth_token=${token}; path=/; max-age=86400; SameSite=Strict; HttpOnly`
  
  // HttpOnly cookie можно установить только на сервере
  // На клиенте используйте API endpoint
  fetch('/api/auth/set-token', {
    method: 'POST',
    body: JSON.stringify({ token }),
  })
}

Важно: для токенов авторизации обязательно используйте HttpOnly флаг — это защита от XSS атак.


domain

// ❌ Плохо: cookie только на текущем домене
document.cookie = "key=value"

// ✅ Хорошо: cookie на всех поддоменах
document.cookie = "key=value; domain=.example.com"

Правило: точка перед доменом включает поддомены.

path

// ❌ Cookie доступна только на /admin
document.cookie = "key=value; path=/admin"

// ✅ Cookie доступна везде
document.cookie = "key=value; path=/"

max-age vs expires

// Вариант 1: max-age (секунды)
document.cookie = "key=value; max-age=31536000" // 1 год

// Вариант 2: expires (дата)
const date = new Date()
date.setFullYear(date.getFullYear() + 1)
document.cookie = `key=value; expires=${date.toUTCString()}`

Рекомендация: используйте max-age — проще и понятнее.

SameSite

// Lax: защита от CSRF, но работает с обычными переходами
document.cookie = "key=value; SameSite=Lax"

// Strict: максимальная защита, но cookie не передаётся при переходе извне
document.cookie = "key=value; SameSite=Strict"

// None: cookie передаётся всегда (требует Secure)
document.cookie = "key=value; SameSite=None; Secure"

Рекомендация:

  • Для настроек (тема, язык) → SameSite=Lax
  • Для авторизации → SameSite=Strict

Secure

// ✅ На HTTPS
document.cookie = "key=value; Secure"

// ❌ На HTTP — cookie НЕ будет сохранена

Правило: Secure работает только на HTTPS. На localhost HTTP работает без Secure.

HttpOnly

// ⚠️ Можно установить только на сервере!
Set-Cookie: auth_token=abc123; HttpOnly; Secure

HttpOnly cookie недоступна для JavaScript — защита от XSS.


🧪 Тестирование на localhost

Проблема

// ❌ НЕ работает на localhost
document.cookie = "theme=dark; domain=.localhost"

// Причина: браузеры не поддерживают wildcard для localhost

Решение 1: Использовать .local домены

Измените /etc/hosts (Linux/Mac) или C:\Windows\System32\drivers\etc\hosts (Windows):

127.0.0.1 example.local
127.0.0.1 app.example.local
127.0.0.1 api.example.local

Теперь cookie работают:

// ✅ Работает
document.cookie = "theme=dark; domain=.example.local"

Решение 2: Условная логика

const isLocalhost = window.location.hostname.includes('localhost')

const cookie = isLocalhost
  ? `theme=dark; path=/` // Без domain на localhost
  : `theme=dark; path=/; domain=.example.com; Secure` // С domain на production
  
document.cookie = cookie

На localhost темы между поддоменами не синхронизируются, но на production всё работает.


🔒 Безопасность

// ❌ ОПАСНО: токен доступен через JavaScript
document.cookie = "auth_token=secret123"

// ✅ БЕЗОПАСНО: HttpOnly cookie (только на сервере)
// Set-Cookie: auth_token=secret123; HttpOnly; Secure; SameSite=Strict

2. Используйте Secure на production

// ❌ Плохо: cookie может быть перехвачена по HTTP
document.cookie = "session=abc"

// ✅ Хорошо: cookie только по HTTPS
document.cookie = "session=abc; Secure"

3. Установите SameSite для защиты от CSRF

// ❌ Без защиты
document.cookie = "key=value"

// ✅ С защитой от CSRF
document.cookie = "key=value; SameSite=Lax"

4. Ограничьте срок жизни

// ❌ Cookie живёт вечно
document.cookie = "key=value"

// ✅ Cookie удалится через 1 день
document.cookie = "key=value; max-age=86400"

📊 Сравнение подходов

Критерий Cookie localStorage
Доступ между поддоменами ✅ Да (с domain=.example.com) ❌ Нет (изолирован по домену)
Доступ из JavaScript ✅ Да (если не HttpOnly) ✅ Да
Отправка на сервер ✅ Автоматически с каждым запросом ❌ Нет
Размер 4 KB на cookie 5-10 MB
Безопасность HttpOnly, Secure, SameSite ⚠️ Доступен для XSS

Вывод: для синхронизации между поддоменами — только Cookie.

Критерий Cookie sessionStorage
Живёт между сессиями ✅ Да (если установлен max-age) ❌ Нет (удаляется при закрытии вкладки)
Доступ между вкладками ✅ Да ❌ Нет
Доступ между поддоменами ✅ Да ❌ Нет

🚀 Best Practices

1. Явно указывайте все параметры

// ❌ Плохо: неясно как долго живёт cookie
document.cookie = "theme=dark"

// ✅ Хорошо: все параметры явно
document.cookie = "theme=dark; path=/; domain=.example.com; max-age=31536000; SameSite=Lax; Secure"
// lib/cookies.ts
export const setCookie = (
  name: string,
  value: string,
  options: {
    domain?: string
    maxAge?: number
    path?: string
    sameSite?: 'Lax' | 'Strict' | 'None'
    secure?: boolean
  } = {}
) => {
  const {
    domain,
    maxAge = 31536000,
    path = '/',
    sameSite = 'Lax',
    secure = false,
  } = options

  let cookie = `${name}=${value}; path=${path}; max-age=${maxAge}; SameSite=${sameSite}`
  
  if (domain) cookie += `; domain=${domain}`
  if (secure) cookie += '; Secure'
  
  document.cookie = cookie
}

export const getCookie = (name: string): string | null => {
  const cookie = document.cookie
    .split('; ')
    .find(row => row.startsWith(`${name}=`))
  
  return cookie ? cookie.split('=')[1] : null
}

export const deleteCookie = (name: string, domain?: string) => {
  let cookie = `${name}=; path=/; max-age=0`
  if (domain) cookie += `; domain=${domain}`
  
  document.cookie = cookie
}

// Использование
setCookie('theme', 'dark', {
  domain: '.example.com',
  secure: true,
  sameSite: 'Lax',
})

const theme = getCookie('theme')
deleteCookie('theme', '.example.com')

3. Проверяйте окружение

const isProduction = process.env.NODE_ENV === 'production'
const domain = isProduction ? '.example.com' : undefined
const secure = isProduction

setCookie('key', 'value', { domain, secure })

4. Логируйте для отладки

if (process.env.NODE_ENV === 'development') {
  console.log('Cookie set:', document.cookie)
  console.log('Domain:', window.location.hostname)
}

⚠️ Частые ошибки

1. Забыли точку перед доменом

// ❌ Плохо: cookie только на example.com (без поддоменов)
document.cookie = "key=value; domain=example.com"

// ✅ Хорошо: cookie на example.com и всех поддоменах
document.cookie = "key=value; domain=.example.com"

2. Использовали Secure на HTTP

// ❌ На http://localhost:3000 cookie НЕ сохранится
document.cookie = "key=value; Secure"

// ✅ Secure только на HTTPS
const isHttps = window.location.protocol === 'https:'
const cookie = isHttps 
  ? "key=value; Secure" 
  : "key=value"

3. Не учли браузерное кэширование

Cookie может кэшироваться браузером. Используйте DevTools для проверки:

  • Chrome: F12 → Application → Cookies
  • Firefox: F12 → Storage → Cookies

4. Превысили лимит размера

// ❌ Cookie больше 4 KB — будет обрезана
const bigData = 'x'.repeat(5000)
document.cookie = `data=${bigData}`

// ✅ Храните большие данные в localStorage
localStorage.setItem('data', bigData)

🎓 Выводы

✅ Используйте, если:

  • Нужна синхронизация между поддоменами
  • Данные небольшие (< 4 KB)
  • Нужна отправка на сервер

❌ Не используйте, если:

  • Данные большие (> 4 KB)
  • Нужна изоляция между доменами
  • Данные чувствительные (используйте HttpOnly на сервере)

Рекомендуемые настройки

Для настроек (тема, язык, валюта):

document.cookie = "setting=value; path=/; domain=.example.com; max-age=31536000; SameSite=Lax; Secure"

Для авторизации (на сервере):

Set-Cookie: auth_token=xxx; path=/; domain=.example.com; max-age=86400; SameSite=Strict; Secure; HttpOnly

📚 Ссылки