Fastify-cookie обеспечивает базовый механизм работы с cookies в
приложениях на Fastify, но сам по себе не решает задачу безопасного
хранения сессионных данных. Для построения защищённой сессии
используется связка cookie + шифрование/подпись через библиотеку Iron
(@hapi/iron), которая позволяет «запечатывать» объект
состояния в строку и безопасно восстанавливать его на сервере.
Для начала подключается набор зависимостей:
npm install fastify @fastify/cookie @hapi/iron
Fastify-cookie регистрируется как плагин, после чего становится доступен механизм чтения и записи cookies в каждом запросе.
import Fastify from 'fastify'
import cookie from '@fastify/cookie'
const app = Fastify()
app.register(cookie, {
secret: 'super-secret-key-base'
})
Параметр secret используется для подписи cookies, что
предотвращает их незаметную подмену на стороне клиента.
Cookie в классической реализации Fastify представляет собой хранилище небольшого объёма данных на клиенте. Однако хранить там напрямую состояние пользователя небезопасно:
Поэтому используется схема:
Iron реализует механизм «sealing/unsealing» — упаковки и распаковки данных с криптографической защитой.
import Iron from '@hapi/iron'
const password = 'complex-32-characters-minimum-secret'
const session = {
userId: 42,
role: 'admin'
}
const sealed = await Iron.seal(session, password, Iron.defaults.encryption)
Результатом является строка, которую безопасно хранить в cookie.
const unsealed = await Iron.unseal(sealed, password, Iron.defaults.encryption)
Если строка была изменена клиентом, расшифровка завершится ошибкой.
Интеграция Iron с Fastify-cookie строится вручную через middleware-логику.
app.post('/login', async (req, reply) => {
const user = {
id: 1,
username: 'admin'
}
const sessionData = await Iron.seal(user, 'complex-32-characters-minimum-secret', Iron.defaults.encryption)
reply.setCookie('session', sessionData, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
maxAge: 60 * 60 * 24
})
return { ok: true }
})
app.get('/profile', async (req, reply) => {
const sessionCookie = req.cookies.session
if (!sessionCookie) {
return reply.code(401).send({ error: 'unauthorized' })
}
const session = await Iron.unseal(
sessionCookie,
'complex-32-characters-minimum-secret',
Iron.defaults.encryption
)
return {
userId: session.id,
username: session.username
}
})
При работе с сессиями критически важна конфигурация cookie:
httpOnly — блокирует доступ из JavaScriptsecure — передача только по HTTPSsameSite — защита от CSRFmaxAge — время жизни сессииreply.setCookie('session', value, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 3600
})
При необходимости можно перезаписывать cookie, обновляя данные пользователя:
app.post('/refresh-session', async (req, reply) => {
const sessionCookie = req.cookies.session
const session = await Iron.unseal(
sessionCookie,
'complex-32-characters-minimum-secret',
Iron.defaults.encryption
)
session.lastActivity = Date.now()
const renewed = await Iron.seal(
session,
'complex-32-characters-minimum-secret',
Iron.defaults.encryption
)
reply.setCookie('session', renewed, {
httpOnly: true,
secure: true,
sameSite: 'strict'
})
return { refreshed: true }
})
Удаление сессии выполняется через очистку cookie:
app.post('/logout', async (req, reply) => {
reply.clearCookie('session', {
path: '/'
})
return { loggedOut: true }
})
Основное преимущество Iron заключается в том, что даже при доступе к cookie клиент не может изменить данные незаметно. Любая модификация строки нарушает криптографическую подпись и делает распаковку невозможной.
Это позволяет хранить в сессии:
без риска подделки.
В архитектуре Fastify-cookie + Iron:
Такое разделение упрощает масштабирование логики сессий без введения серверного хранилища.
Использование зашифрованных cookie-сессий накладывает ряд ограничений:
Для более сложных систем часто комбинируют этот подход с серверным хранилищем токенов или Redis-сессиями.
app.addHook('preHandler', async (req, reply) => {
const cookie = req.cookies.session
if (!cookie) {
req.session = null
return
}
try {
req.session = await Iron.unseal(
cookie,
'complex-32-characters-minimum-secret',
Iron.defaults.encryption
)
} catch {
req.session = null
}
})
После этого обработчики получают доступ к req.session
без повторного декодирования.
Ключевой момент — длина и хранение секретного ключа:
const PASSWORD = process.env.SESSION_SECRET
Ошибки в управлении ключом приводят к полной компрометации всей системы сессий, поскольку старые cookie становятся недействительными при смене секрета.
При невозможности расшифровать cookie типичный сценарий:
try {
const session = await Iron.unseal(...)
} catch (err) {
reply.clearCookie('session')
return reply.code(401).send()
}
Подход с Fastify-cookie и Iron хорошо подходит для:
Но становится менее удобным при: