Синхронизация данных между поддоменами через Cookie
Как передавать данные между основным доменом и поддоменами с помощью Cookie. Разбираем настройку domain, SameSite и решаем проблемы с localhost.
Синхронизация данных между поддоменами через 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 с правильным domain
Как работают Cookie и домены
По умолчанию 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 атак.
⚙️ Параметры Cookie
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 всё работает.
🔒 Безопасность
1. Не храните секреты в обычных Cookie
// ❌ ОПАСНО: токен доступен через 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 vs localStorage
| Критерий | Cookie | localStorage |
|---|---|---|
| Доступ между поддоменами | ✅ Да (с domain=.example.com) |
❌ Нет (изолирован по домену) |
| Доступ из JavaScript | ✅ Да (если не HttpOnly) |
✅ Да |
| Отправка на сервер | ✅ Автоматически с каждым запросом | ❌ Нет |
| Размер | 4 KB на cookie | 5-10 MB |
| Безопасность | ✅ HttpOnly, Secure, SameSite |
⚠️ Доступен для XSS |
Вывод: для синхронизации между поддоменами — только Cookie.
Cookie vs sessionStorage
| Критерий | 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"
2. Создайте утилиту для работы с Cookie
// 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)
🎓 Выводы
Когда использовать Cookie с domain?
✅ Используйте, если:
- Нужна синхронизация между поддоменами
- Данные небольшие (< 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