РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ Javascript разработчик OZON - MIDDLE
Сегодня мы разберем живое техническое интервью фронтенд-разработчика с акцентом на финтех: от требований к высоконагруженной платежной системе и работе с безопасностью до вопросов по HTTP, кукам и реализации авторизации через токены. В ходе беседы кандидат демонстрирует базовое понимание сетевого взаимодействия и принципов работы протоколов, а также решает алгоритмическую задачу, что позволяет оценить его уровень подготовки и стиль мышления на практике.
Вопрос 1. Знаком ли кандидат с информацией о финтех-направлении и вакансии, которую ранее предоставила команда подбора?
Таймкод: 00:00:45
Ответ собеседника: правильный. Кандидат подтверждает, что получил общее представление о финтех-направлении, возможностях и команде платежей.
Правильный ответ:
Да, я уже ознакомился с информацией, которую предоставила команда подбора: общим описанием финтех-направления, ключевыми продуктами, ролью платёжной команды и текущими задачами. Если потребуется, могу уточнить детали по архитектуре платежей, процессам интеграции, требованиям к надежности и соответствию регуляторике, чтобы лучше сопоставить свой опыт с задачами команды.
Вопрос 2. Что происходит в сети после ввода URL в браузере и нажатия Enter (без деталей отрисовки страницы)?
Таймкод: 00:04:15
Ответ собеседника: неполный. Описал DNS-резолвинг домена в IP, использование кешей, установку TCP-соединения, использование HTTP/HTTPS и отправку запроса на сервер.
Правильный ответ:
Процесс можно разбить на несколько логических этапов: разбор URL, определение IP-адреса, установление соединения, шифрование (для HTTPS), отправка HTTP-запроса, получение ответа. Рассмотрим подробно.
- Разбор URL
- Браузер разбирает URL на части:
- схема: http / https
- домен: example.com
- порт: по умолчанию 80 для HTTP, 443 для HTTPS (или указанный явно)
- путь, query-параметры, фрагмент
- На основе схемы выбирается протокол прикладного уровня (HTTP/HTTPS) и дальнейшее поведение (например, необходимость TLS).
- Проверка кешей и HSTS/редиректы
- Перед реальным запросом могут сработать:
- кешированные редиректы (например, ранее полученный 301)
- HSTS (браузер знает, что с доменом можно работать только по HTTPS и сразу меняет http -> https)
- кеш DNS записей в самом браузере.
- Определение IP-адреса (DNS-резолвинг) Браузер получает IP-адрес домена. Последовательность:
- Проверка:
- кеш браузера
- кеш ОС
- файл hosts (локальные переопределения)
- Если записи нет, ОС обращается к настроенным DNS-серверам (например, 8.8.8.8 или DNS провайдера).
- Последовательность DNS-запросов может включать:
- рекурсивный DNS-сервер провайдера или публичный
- корневые DNS-серверы
- TLD-серверы (.com, .ru и т.д.)
- авторитативный DNS-сервер домена
- В итоге возвращается запись:
- A/AAAA (IPv4/IPv6 адрес)
- возможные CNAME (alias), которые доп. резолвятся
- Результат кешируется на различных уровнях (с учётом TTL).
- Установление сетевого соединения (TCP) После получения IP:
- Определяется порт (по схеме или явно из URL).
- Устанавливается TCP-соединение (если его ещё нет и не используется keep-alive / connection pooling):
- трехстороннее рукопожатие:
- клиент → сервер: SYN
- сервер → клиент: SYN+ACK
- клиент → сервер: ACK
- трехстороннее рукопожатие:
- На этом этапе формируется канал передачи данных с гарантированной доставкой и упорядочиванием сегментов.
- TLS-рукопожатие (для HTTPS) Если используется HTTPS, поверх TCP накладывается TLS:
- Клиент отправляет:
- ClientHello (версии протокола, поддерживаемые шифры, поддержка ALPN, SNI — домен, к которому обращаемся).
- Сервер отвечает:
- ServerHello (выбор шифра и параметров),
- сертификат сервера,
- дополнительные параметры для обмена ключами.
- Клиент:
- проверяет сертификат:
- цепочку доверия (CA),
- срок действия,
- домен (CN/SAN должен соответствовать),
- статус отзыва (OCSP/CRL при необходимости).
- генерирует и/или согласует с сервером сессионные ключи.
- проверяет сертификат:
- После успешного рукопожатия:
- устанавливается зашифрованный канал.
- Возможны оптимизации:
- TLS session resumption,
- 0-RTT в TLS 1.3.
Важно: если сертификат недействителен или домен не совпадает, корректный клиент должен показать предупреждение и не продолжать (если пользователь не проигнорирует вручную).
- Отправка HTTP-запроса Теперь браузер формирует и отправляет HTTP-запрос (внутри TCP/TLS-соединения):
Пример (HTTP/1.1):
GET /path?param=value HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml
Accept-Language: ru-RU,ru;q=0.9
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Ключевые моменты:
- Метод (GET/POST/PUT/DELETE/…)
- Путь (без домена)
- Заголовок Host (обязателен в HTTP/1.1, нужен для виртуального хостинга)
- Cookies, авторизация, дополнительные заголовки.
- В HTTP/2 и HTTP/3 формат фреймов отличается, но суть та же: отправка логически того же запроса по более эффективному протоколу.
Выбор версии протокола:
- В HTTPS через ALPN определяется HTTP/1.1 или HTTP/2/3.
- HTTP/3 использует поверх UDP протокол QUIC, но логика для разработчика похожа: запрос/ответ.
- Обработка запроса на сервере На стороне сервера:
- Запрос попадает на:
- L4/L7 балансировщик,
- reverse proxy (NGINX/Envoy/Traefik),
- приложение (Go-сервис и т.д.).
- Сервер:
- читает заголовки,
- определяет роут,
- выполняет бизнес-логику,
- может обращаться к БД, кэшам, другим сервисам,
- формирует ответ.
Пример ответа:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Cache-Control: no-cache
<html>...</html>
- Доставка ответа и завершение/переиспользование соединения
- Ответ возвращается по тому же соединению.
- Браузер:
- может повторно использовать соединение (keep-alive) для последующих запросов к тому же хосту.
- При отсутствии дальнейших запросов:
- соединение закрывается (FIN/ACK) или по инициативе одной из сторон.
Пример на Go: минимальный HTTP-сервер, который участвует в этом процессе
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK\n")
}
func main() {
http.HandleFunc("/", handler)
// Запускает HTTP-сервер на порту 8080.
// Go рантайм сам обрабатывает TCP accept, чтение HTTP-запроса, парсинг и т.д.
log.Println("Listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
Это звено «серверной стороны», которое принимает тот самый HTTP-запрос, сформированный после всех сетевых шагов, описанных выше.
Вопрос 3. Как устанавливается сетевое соединение между клиентом и сервером при использовании TCP, и какие дополнительные уровни соединений существуют, например, в случае HTTPS?
Таймкод: 00:05:21
Ответ собеседника: неполный. Упомянул TCP как протокол с гарантированной доставкой и запрос-ответ, но некорректно описал установленное соединение и трехстороннее рукопожатие. Про SSL/TLS и дополнительные уровни сказал поверхностно.
Правильный ответ:
Для понимания сетевого взаимодействия важно разделять уровни: транспортный (TCP), прикладной (HTTP) и криптографический (TLS). Соединение в сети — это не одно «волшебное» действие, а последовательность шагов и уровней.
Разберём сначала TCP, затем TLS как надстройку, и как это выглядит для HTTPS.
Основные свойства TCP
TCP решает задачи:
- надежная доставка (повторная отправка утерянных сегментов),
- упорядочивание данных,
- контроль перегрузки и потока,
- установление и завершение соединения. TCP — потоковый протокол: он не знает о «запросах» и «ответах», это уже логика над ним (HTTP, gRPC и т.п.).
Установление TCP-соединения: трехстороннее рукопожатие (3-way handshake)
Когда клиент хочет подключиться к серверу (IP:порт):
-
Клиент → Сервер: SYN
- Клиент посылает сегмент с флагом SYN и начальным номером последовательности (ISN — Initial Sequence Number).
- Говорит: «Хочу установить соединение, мой стартовый seq = X».
-
Сервер → Клиент: SYN+ACK
- Сервер, если порт слушает и готов, отвечает:
- SYN с собственным ISN = Y,
- ACK с подтверждением получения X+1.
- Говорит: «Я согласен, мой seq = Y, твой X получил».
- Сервер, если порт слушает и готов, отвечает:
-
Клиент → Сервер: ACK
- Клиент подтверждает получение SYN+ACK:
- ACK с номером Y+1.
- Соединение установлено.
- Клиент подтверждает получение SYN+ACK:
После этого обе стороны считают, что:
- имеется логический двунаправленный канал,
- они согласовали начальные номера последовательностей,
- могут надежно обмениваться байтовым потоком.
Поддержание соединения
TCP-соединение поддерживается, пока:
- стороны отправляют данные или периодически поддерживают активность,
- не произошёл timeout (простой слишком долгий),
- одна из сторон явно не закрыла соединение (FIN) или аварийно (RST).
Закрытие соединения — это отдельный обмен (обычно 4-way handshake: FIN/ACK в каждую сторону), потому что каждый поток (client→server и server→client) закрывается независимо.
Важно:
- TCP сам по себе не гарантирует «постоянное соединение» навсегда.
- Над ним могут быть:
- keep-alive на уровне TCP,
- keep-alive на уровне HTTP,
- ping/pong в протоколах типа WebSocket,
- health-check’и и heartbeat’ы в собственных протоколах.
Дополнительные уровни: TLS поверх TCP (HTTPS как HTTP поверх TLS поверх TCP)
Когда мы говорим об «ещё одном уровне соединения» в контексте HTTPS — речь про TLS-рукопожатие, которое накладывается поверх уже установленного TCP-соединения.
Слои выглядят так:
- для HTTP:
- HTTP → TCP → IP → ...
- для HTTPS:
- HTTP → TLS → TCP → IP → ...
Последовательность для HTTPS:
- Установление TCP-соединения (описано выше).
- Установление TLS-сессии (TLS Handshake).
- Передача HTTP-запросов внутри зашифрованного канала.
Ключевые шаги TLS (упрощенно, актуально для TLS 1.2/1.3):
-
Клиент → Сервер: ClientHello
- поддерживаемые версии TLS,
- набор шифров (cipher suites),
- параметры, расширения (ALPN для выбора HTTP/1.1 или HTTP/2, SNI — домен).
-
Сервер → Клиент: ServerHello (+ сертификат, параметры ключей)
- выбирает версию и шифр,
- отправляет сертификат, подтверждающий подлинность домена,
- в TLS 1.3 — параметры для установления общего секрета (ECDHE).
-
Проверка сертификата клиентом:
- цепочка доверия (корневые/промежуточные CA),
- домен (CN/SAN совпадает с запрошенным),
- срок действия,
- отсутствие отзыва (OCSP/CRL — опционально).
-
Установление секретных ключей:
- на основе алгоритма (обычно ECDHE) клиент и сервер вычисляют общий сеансовый ключ.
- Дальнейший трафик шифруется симметричным шифром.
-
Завершение рукопожатия:
- стороны обмениваются подтверждениями, что все параметры согласованы и можно шифровать.
После этого:
- Канал TCP остается тем же,
- Но поверх него создаётся логический защищенный канал TLS,
- Никакой HTTP-трафик не идет в открытом виде.
Повторное использование и оптимизации:
- TLS session resumption: повторные подключения быстрее, без полного хэндшейка.
- ALPN: выбор HTTP/2/3.
- В TLS 1.3 — меньше RTT, возможен 0-RTT для повторных подключений (с нюансами безопасности).
Четкое разделение:
- TCP-соединение: отвечает за доставку байтов.
- TLS-сессия: отвечает за шифрование и аутентификацию сторон.
- HTTP: логика запрос/ответ, заголовки, методы.
Пример на Go: установка TCP и TLS
Прямое TCP-соединение:
package main
import (
"log"
"net"
)
func main() {
conn, err := net.Dial("tcp", "example.com:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Здесь мы можем вручную писать HTTP-запрос в conn.
// conn представляет установленное TCP-соединение.
}
TCP + TLS (аналог HTTPS без высокого уровня http.Client):
package main
import (
"crypto/tls"
"fmt"
"log"
)
func main() {
// Устанавливаем TLS поверх TCP
conn, err := tls.Dial("tcp", "example.com:443", &tls.Config{
ServerName: "example.com", // важно для проверки сертификата и SNI
})
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Отправляем HTTP-запрос по зашифрованному каналу
fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")
buf := make([]byte, 4096)
n, _ := conn.Read(buf)
fmt.Println(string(buf[:n]))
}
Итого:
- «Соединение» между клиентом и сервером в TCP — это результат трехстороннего рукопожатия и последующей поддержки состояния (номера последовательностей, окон, таймеров).
- В HTTPS поверх уже установленного TCP-соединения дополнительно поднимается TLS-сессия — это действительно дополнительный «логический уровень соединения» для шифрования и аутентификации.
- HTTP запросы/ответы — это уже прикладной протокол, который использует эти уровни и не управляет непосредственно ни TCP, ни TLS, а лишь строится сверху.
Вопрос 4. Что такое HTTP-запрос и из чего он состоит?
Таймкод: 00:07:25
Ответ собеседника: неправильный. Вместо структуры HTTP-запроса ушёл в обсуждение TCP, REST-ручек, юзкейсов и отличий HTTP/1.1/HTTP/2, не раскрыв базовые части HTTP-запроса.
Правильный ответ:
HTTP-запрос — это сообщение прикладного уровня, которое клиент (обычно браузер или сервис) отправляет серверу в рамках протокола HTTP. Он описывает:
- какое действие нужно выполнить (метод),
- к какому ресурсу обратиться (URL / путь),
- с какими параметрами и контекстом (заголовки, тело),
- при необходимости — с данными (JSON, форма, файл и т.п.).
Классический HTTP/1.1-запрос состоит из трех основных частей:
- Стартовая строка (Request Line)
- Заголовки (Headers)
- Пустая строка и, опционально, тело (Body)
Рассмотрим подробно.
- Стартовая строка (Request Line)
Формат:
- METHOD SP REQUEST-URI SP HTTP-VERSION CRLF
Где:
- METHOD — действие:
- GET — получить ресурс;
- POST — создать ресурс или отправить данные;
- PUT — полная замена ресурса;
- PATCH — частичное изменение;
- DELETE — удаление;
- HEAD — как GET, но без тела ответа;
- OPTIONS — какие методы/возможности поддерживаются;
- и др.
- REQUEST-URI — путь и query:
- Например: /api/v1/users?active=true
- HTTP-VERSION:
- HTTP/1.0, HTTP/1.1 (в HTTP/2 и HTTP/3 формат другой на уровне фреймов, но логически поля те же).
Пример:
GET /api/v1/users?active=true HTTP/1.1
Ключевой смысл:
- Стартовая строка определяет «что делаем» и «над чем».
- Заголовки (Headers)
Следом идёт набор строк формата:
- Name: Value
Они передают метаинформацию о запросе:
- Кто мы,
- Что отправляем,
- Что ожидаем в ответ,
- Как обрабатывать соединение, кэш, язык и т.д.
Ключевые заголовки:
-
Host:
- Обязателен в HTTP/1.1.
- Позволяет одному IP обслуживать несколько доменов (виртуальный хостинг).
- Пример:
Host: example.com
-
User-Agent:
- Идентификация клиента.
- Пример:
User-Agent: Mozilla/5.0 ...
-
Accept:
- Какие типы контента клиент готов принять.
- Пример:
Accept: application/json
-
Accept-Language:
- Предпочитаемый язык.
- Пример:
Accept-Language: ru-RU,ru;q=0.9
-
Content-Type:
- Тип тела запроса (если есть).
- Пример:
Content-Type: application/jsonContent-Type: application/x-www-form-urlencodedContent-Type: multipart/form-data; boundary=...
-
Content-Length / Transfer-Encoding:
- Как определить длину тела (байты или chunked-передача).
-
Authorization:
- Токены/учетные данные.
- Пример:
Authorization: Bearer <jwt>
-
Cookie:
- Куки, связанные с доменом.
- Пример:
Cookie: session_id=abc123;
-
Connection:
- Управление соединением.
- Пример:
Connection: keep-alive
После списка заголовков всегда идет пустая строка:
...
Header-N: valueN
- Тело запроса (Body)
Тело присутствует не всегда:
- У GET, DELETE чаще без тела (по спецификации тело допустимо, но обычно не используют).
- У POST, PUT, PATCH почти всегда есть тело.
Типичные варианты тела:
- JSON:
Content-Type: application/json
{"name": "Ivan", "age": 30} - Формы (HTML):
application/x-www-form-urlencodedmultipart/form-data(файлы, формы)
- XML, Protobuf, MsgPack и т.д.
Заголовки определяют:
- формат (
Content-Type), - размер (
Content-LengthилиTransfer-Encoding).
Пример полного HTTP/1.1-запроса:
POST /api/v1/payments HTTP/1.1
Host: payments.example.com
User-Agent: Go-http-client/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Length: 73
Connection: keep-alive
{
"user_id": 12345,
"amount": 500.00,
"currency": "RUB",
"method": "card"
}
Здесь видно:
- Стартовая строка:
- POST /api/v1/payments HTTP/1.1
- Заголовки:
- кто мы, к кому, чем авторизуемся, какой формат данных.
- Тело:
- JSON c параметрами платежа.
Взаимосвязь с TCP и HTTP/2/3 (кратко, без повторов):
- В HTTP/1.1 этот текстовый запрос целиком передаётся внутри TCP-соединения.
- В HTTP/2/3 структура логически та же (метод, путь, заголовки, тело), но передаётся бинарными фреймами. Для разработчика прикладного уровня смысл компонентов запроса не меняется:
- есть метод,
- есть путь,
- есть заголовки,
- есть тело.
Пример построения HTTP-запроса в Go
- С использованием стандартного клиента:
package main
import (
"bytes"
"encoding/json"
"log"
"net/http"
"time"
)
func main() {
body := map[string]interface{}{
"user_id": 12345,
"amount": 500.00,
"currency": "RUB",
"method": "card",
}
data, _ := json.Marshal(body)
req, err := http.NewRequest(http.MethodPost, "https://payments.example.com/api/v1/payments", bytes.NewReader(data))
if err != nil {
log.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer <token>")
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
log.Println("Status:", resp.Status)
}
- Ручная сборка «сырых» данных (понимание структуры):
raw := "GET /api/v1/users?active=true HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"Accept: application/json\r\n" +
"Connection: close\r\n\r\n"
Кратко:
- HTTP-запрос — это строго структурированное сообщение.
- Ключевые части: стартовая строка, заголовки, опциональное тело.
- Понимание структуры важно для:
- отладки (tcpdump, curl, Postman),
- написания клиентов/прокси,
- безопасности (подделка заголовков, корректный парсинг),
- оптимизации API на уровне протокола, а не только бизнес-логики.
Вопрос 5. В чем различия между методами GET и POST в HTTP, в том числе в контексте кеширования и передачи данных?
Таймкод: 00:08:44
Ответ собеседника: неполный. Упомянул кеширование GET из-за query-параметров и то, что POST не кешируется, сказал про возможность тела у GET, но не раскрыл семантику, идемпотентность и реальные правила кеширования, допустил неточности.
Правильный ответ:
Различия между GET и POST — это не только «один с query, другой с body», а в первую очередь семантика, идемпотентность, кеширование, безопасные операции и использование в архитектуре API.
Основные аспекты:
- Семантика методов
-
GET:
- Семантически: запросить представление ресурса.
- Должен быть:
- безопасным — не изменять состояние на сервере;
- идемпотентным — повторный запрос не меняет результат с точки зрения состояния ресурса.
- Примеры:
- получение профиля пользователя,
- получение списка транзакций,
- проверка статуса платежа.
-
POST:
- Семантически: создать подресурс или инициировать обработку/операцию, по результату которой может измениться состояние.
- Не гарантирует идемпотентность.
- Используется для:
- создания ресурса (создать платеж),
- выполнения команды (инициировать перевод),
- сложных запросов, когда параметры неуместны в URL.
- Если один и тот же POST отправлен дважды — это могут быть два платежа, два заказа и т.п.
Важно: спецификация не запрещает серверу делать GET с побочным эффектом или POST — идемпотентным, но это нарушение устоявшейся семантики и ломает кэширование, ретраи и инфраструктуру.
- Передача данных: URL vs тело
-
GET:
- Данные обычно передаются в:
- query-параметрах:
/payments?user_id=123&status=success - path-параметрах:
/users/123/payments
- query-параметрах:
- Тело формально не запрещено в некоторых версиях спецификаций, но:
- на практике широко не используется,
- многие прокси/серверы/клиенты игнорируют или не ожидают тело у GET.
- Поэтому для предикатов поиска, фильтрации и навигации — query и путь.
- Данные обычно передаются в:
-
POST:
- Данные передаются в теле запроса:
application/jsonapplication/x-www-form-urlencodedmultipart/form-data- бинарные форматы (protobuf и т.п.)
- Размер тела POST обычно может быть существенно больше, чем типичный безопасный размер URL.
- Данные передаются в теле запроса:
Пример:
GET /api/v1/payments?user_id=123&limit=10 HTTP/1.1
Host: api.example.com
Accept: application/json
POST /api/v1/payments HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"user_id": 123,
"amount": 500.00,
"currency": "RUB",
"method": "card"
}
- Идемпотентность и безопасность
-
Безопасность (safe):
- GET: безопасный — не должен менять состояние на сервере.
- POST: небезопасный — предполагает изменения.
-
Идемпотентность:
- GET: идемпотентен. 10 одинаковых GET-запросов должны иметь тот же эффект, что и один (с точки зрения состояния).
- POST: неидемпотентен. Повторный запрос может создать дубль транзакции, заказа, перевода.
Это важно для:
- ретраев запросов (клиенты, балансировщики, прокси),
- проектирования платежных и финансовых API:
- для создания транзакции — POST,
- для проверки статуса — GET.
В финансовых системах из-за этого часто используют:
- POST с idempotency key в заголовках (например,
Idempotency-Key) или в теле:- чтобы сервер сам сделал операцию идемпотентной, даже на уровне POST.
- Кеширование
Реальные правила кеширования завязаны не только на метод, но и на заголовки. Однако:
-
GET:
- Может кешироваться:
- браузером,
- прокси,
- CDN.
- По умолчанию кеширующие прокси имеют право кешировать успешные GET-ответы, если заголовки не запрещают.
- Управление:
Cache-Control,Expires,ETag,Last-Modified.
- Query-параметры:
- используются для вариативности (фильтры, страницы, версии),
- не делают запрос «автоматически кешируемым», но CDN/прокси обычно умеют кешировать конкретные URL целиком.
- Может кешироваться:
-
POST:
- По спецификации может быть кеширован, но:
- по умолчанию кеш не обязан его кешировать,
- требует явных заголовков (
Cache-ControlиExpires) и поддержки со стороны кеширующего прокси.
- На практике POST почти всегда рассматривается как некешируемый, особенно для операций с изменением состояния и чувствительных данных (платежи, логины).
- По спецификации может быть кеширован, но:
Ключевой практический вывод:
- GET — основной кандидат для чтения и кеширования.
- POST — для операций, которые изменяют состояние или содержат чувствительные данные, и обычно не кешируется.
- URL длина и практические ограничения
- GET:
- URL имеет ограничения по длине в браузерах и серверах (часто от ~2К до ~8К+ байт).
- Не подходит для очень большого объёма данных (особенно бинарных).
- POST:
- Подходит для больших payload’ов (файлы, большие JSON и т.п.), ограничения задаются сервером (например,
client_max_body_sizeв Nginx).
- Подходит для больших payload’ов (файлы, большие JSON и т.п.), ограничения задаются сервером (например,
- Примеры на Go
GET-запрос (чтение ресурса, кешируемый, безопасный):
package main
import (
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://api.example.com/api/v1/payments?user_id=123&limit=10")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
log.Println("Status:", resp.Status)
}
POST-запрос (создание ресурса, неидемпотентный):
package main
import (
"bytes"
"encoding/json"
"log"
"net/http"
)
func main() {
body := map[string]interface{}{
"user_id": 123,
"amount": 500.00,
"currency": "RUB",
"method": "card",
}
data, _ := json.Marshal(body)
resp, err := http.Post("https://api.example.com/api/v1/payments",
"application/json", bytes.NewReader(data))
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
log.Println("Status:", resp.Status)
}
- Типичные ошибки и то, чего стоит избегать
- Использовать GET для операций, изменяющих состояние (типа
/doTransfer?amount=500):- ломает кеширование,
- небезопасно (логирование URL, история браузера).
- Игнорировать семантику и идемпотентность при проектировании API:
- усложняет ретраи, балансировку, работу gateway’ев.
- Полагать, что «GET кешируется всегда, POST никогда»:
- важно понимать роль заголовков и поведения кеширующих прокси.
Кратко:
- GET — безопасен, идемпотентен, обычно без тела, данные в URL, хорошо кешируется.
- POST — для изменений/создания, с телом, неидемпотентен по умолчанию, кешируется редко и только при явной конфигурации.
Вопрос 6. Что такое cookies в браузере, как они устроены и зачем используются?
Таймкод: 00:09:14
Ответ собеседника: неполный. Упомянул, что cookies хранятся в браузере, доступны фронтенду и бэкенду, есть ограничения по размеру и формат key-value, но не раскрыл основное назначение (сессии, персонализация, трекинг), важные атрибуты и аспекты безопасности.
Правильный ответ:
Cookies — это небольшой фрагмент данных (key-value + набор атрибутов), который сервер (или JavaScript) сохраняет в браузере, а браузер автоматически отправляет обратно на сервер при последующих запросах к соответствующему домену/пути. Это один из ключевых механизмов поддержания состояния поверх статлесс-протокола HTTP.
Основные свойства cookies:
- Хранятся на стороне клиента (браузер).
- Ассоциированы с доменом и путем.
- Автоматически прикладываются браузером к HTTP-запросам, если удовлетворяют правилам (domain, path, протокол, флаги безопасности).
- Имеют ограничения:
- Обычно до ~4KB на cookie.
- Ограничения по количеству cookie на домен.
- Используются для:
- идентификации сессий,
- хранения токенов (при правильном дизайне),
- персонализации интерфейса,
- трекинга (аналитика, реклама),
- сохранения пользовательских настроек.
Структура cookies
На уровне HTTP:
- Установка cookie сервером:
- Сервер в ответе отправляет заголовок Set-Cookie:
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; HttpOnly; Secure; Path=/; SameSite=Lax
Set-Cookie: theme=dark; Path=/; Max-Age=3600
Content-Type: text/html
<html>...</html>
- Отправка cookie браузером:
- При последующих запросах к тому же домену/пути и при соблюдении условий браузер добавляет:
GET /profile HTTP/1.1
Host: example.com
Cookie: session_id=abc123; theme=dark
Базовый формат:
- name=value; атрибут1; атрибут2; ...
Где name/value — строковые пары (URL- или спец-формат кодирования при необходимости).
Ключевые атрибуты cookies
Эти атрибуты определяют, когда и как cookie будут отправляться, и важны для безопасности.
-
Domain:
- Определяет, для какого домена cookie действителен.
- Пример:
Domain=example.com— будет отправляться на example.com и все поддомены: api.example.com, www.example.com.- Без явного Domain cookie привязан к текущему хосту.
- Важно не выставлять слишком широкий домен без необходимости, чтобы ограничить поверхность атаки.
-
Path:
- Ограничивает путь URL, для которых cookie будет отправляться.
- Например:
Path=/api— cookie будет отправляться только для /api/...
-
Expires / Max-Age:
- Определяют время жизни cookie.
- Session cookie:
- без Expires/Max-Age,
- живут до закрытия браузера (или вкладки, в зависимости от реализации).
- Persistent cookie:
- имеют дату истечения (Expires) или время жизни в секундах (Max-Age).
-
Secure:
- Cookie отправляется только по HTTPS.
- Обязателен для чувствительных данных (сессии, токены).
- Предотвращает утечку через незашифрованный HTTP.
-
HttpOnly:
- Запрещает доступ к cookie из JavaScript (document.cookie).
- Защищает от кражи cookie через XSS (не полностью, но сильно снижает риск хищения сессии).
- Для session_id и auth-кук это стандарт де-факто.
-
SameSite:
- Управляет, будут ли cookie отправляться в кросс-сайтовых запросах.
- Значения:
- Strict — не отправлять cookie при переходах с других сайтов (максимальная защита от CSRF).
- Lax — отправлять только в «безопасных» сценариях (навигация ссылкой, GET-запросы верхнего уровня).
- None — отправлять во всех кросс-сайтовых запросах; ТРЕБУЕТ Secure.
- Это критичный атрибут для защиты от CSRF.
Для чего используются cookies
- Идентификация сессий
Частый сценарий:
- Пользователь логинится (логин/пароль, SSO и т.п.).
- Сервер создает сессионную запись в БД/хранилище:
- session_id -> user_id, роль, контекст.
- Отправляет клиенту:
Set-Cookie: session_id=abc123; HttpOnly; Secure; Path=/; SameSite=Lax
- При следующем запросе браузер добавляет:
Cookie: session_id=abc123
- Сервер по session_id находит пользователя и понимает, кто делает запрос.
Это базовый механизм авторизации в веб-приложениях.
- Настройки пользователя и персонализация
Примеры:
- Тема интерфейса:
theme=dark
- Выбранный язык:
lang=ru
- AB-тесты, предпочтения фильтров и т.п.
Тут данные могут быть не чувствительны, часто могут читаться JavaScript’ом.
- Трекинг и аналитика
- Cookie могут использоваться для:
- идентификации уникальных пользователей,
- сквозной аналитики (user_id/anonymous_id),
- рекламных меток (3rd party cookies).
- Современные браузеры и политики приватности (Safari, Firefox, Chrome) постепенно ужесточают правила для third-party cookies.
- Безопасность и антифрод
В финтех и чувствительных системах:
- Cookies могут содержать:
- session_id с привязкой к устройству/IP/fingerprint,
- маркеры для доп. валидации (например, риск-оценка, device-id).
- Важно:
- не класть в cookie секреты в открытом виде (ключи, ПИНы, полные токены без шифрования),
- использовать HttpOnly + Secure + SameSite.
Типичные практики и анти-паттерны
-
Хорошие практики:
- session cookie:
- HttpOnly,
- Secure,
- SameSite=Lax или Strict для защиты от CSRF.
- Шифровать или подписывать значения, чтобы их нельзя было подделать.
- Минимизировать объем и число cookies (каждый запрос несет их в заголовках).
- session cookie:
-
Плохие практики:
- класть в cookie JWT без HttpOnly → легко воруется через XSS;
- класть PII и чувствительные данные в открытом виде;
- выставлять Domain на верхний домен без необходимости;
- использовать cookies без SameSite в современных браузерах — риск CSRF.
Пример установки cookies в Go (сервер)
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
// Допустим, пользователь успешно аутентифицирован и мы создали sessionID.
sessionID := "abc123"
cookie := &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode, // разумный дефолт
// MaxAge или Expires можно задать для "remember me"
}
http.SetCookie(w, cookie)
w.Write([]byte("OK"))
})
Пример чтения cookies в Go:
http.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
sessionID := cookie.Value
// По sessionID получаем пользователя из хранилища...
_ = sessionID
w.Write([]byte("user profile"))
})
Кратко:
- Cookies — механизм хранения и автоматической передачи небольших данных между браузером и сервером.
- Основные цели: сессии, настройки, аналитика.
- Критично понимать и правильно настроить атрибуты (HttpOnly, Secure, SameSite, Domain, Path), особенно в системах с аутентификацией и финансовыми операциями.
Вопрос 7. Что такое cookies, какие у них есть атрибуты (path, HttpOnly, Secure) и для чего они используются на практике?
Таймкод: 00:09:48
Ответ собеседника: правильный. Описал cookies как key-value с атрибутами, корректно объяснил:
- path — ограничение области отправки;
- HttpOnly — недоступность из JS;
- Secure — отправка только по HTTPS. Привёл релевантный пример использования для JWT/refresh-токенов и автоматического обновления.
Правильный ответ:
Cookies — это механизм хранения небольших данных на стороне клиента, который позволяет поддерживать состояние поверх HTTP. Браузер автоматически отправляет cookies в заголовке Cookie при каждом запросе к домену и пути, для которых они действуют. Это ключевой инструмент для аутентификации, сессий, персонализации и некоторых технических механизмов (A/B тесты, трекинг).
Кратко про ключевые атрибуты, упомянутые в вопросе.
- Атрибут path
- Определяет, для какого префикса пути URL cookie будет отправляться.
- Примеры:
Path=/— отправляется для всех путей на домене.Path=/api— отправляется только для/api/..., но не для/admin/....
- Практическое применение:
- Ограничение области видимости cookies между разными приложениями/подсистемами.
- Разделение сессий/настроек по зонам приложения.
- Это уменьшает утечку лишних данных и немного снижает поверхность атак.
- Атрибут HttpOnly
- Если установлен, cookie недоступен из JavaScript (
document.cookie):- браузер продолжает автоматически отправлять его на сервер,
- но JS-код не может прочитать или модифицировать его.
- Практическое применение:
- Использование для сессионных идентификаторов и токенов аутентификации.
- Минимизация риска кражи cookie через XSS:
- даже если злоумышленник внедрит скрипт, прочитать HttpOnly-cookie он не сможет.
- Обязательный атрибут для всех чувствительных cookies (session_id, auth-token).
- Атрибут Secure
- Если установлен, cookie отправляется только по HTTPS:
- не будет отправлен по незашифрованному HTTP даже на тот же домен.
- Практическое применение:
- Защита от утечки сессионных токенов в открытом трафике.
- В продакшене для auth-cookie и любых чувствительных данных должен использоваться всегда.
- В связке с HttpOnly и SameSite формирует базовый уровень защиты для аутентификации.
- Практическое использование cookies
Типичные сценарии:
-
Сессионная аутентификация:
- Сервер генерирует
session_id, сохраняет в хранилище (Redis/SQL) и отдает в HttpOnly+Secure cookie. - Браузер автоматически отправляет cookie при каждом запросе, сервер по session_id определяет пользователя.
- Сервер генерирует
-
Хранение JWT/refresh-токенов:
- Частая практика: access-токен в заголовке Authorization, refresh-токен в HttpOnly+Secure cookie.
- Это позволяет:
- защитить refresh-токен от JS/XSS,
- автоматизировать обновление сессии на бекенде,
- снизить риск компрометации долгоживущего токена.
-
Настройки пользователя:
- Тема интерфейса, выбранный язык и т.п. (можно не HttpOnly, доступно JS).
- Пример:
theme=dark; Path=/;
-
Анти-CSRF и безопасность:
- Cookies в связке с SameSite и/или CSRF-токенами.
- Правильная комбинация атрибутов (HttpOnly, Secure, SameSite) — критична для платежных и любых чувствительных систем.
- Пример установки cookies в Go
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
// Пользователь успешно аутентифицирован
sessionID := "abc123"
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: true,
// SameSite желательно явно указать в реальных системах
})
w.Write([]byte("OK"))
})
При последующих запросах браузер сам добавит:
Cookie: session_id=abc123
если URL подходит под домен+path и используется HTTPS (для Secure).
Таким образом:
- path ограничивает область применения,
- HttpOnly защищает от чтения из JS,
- Secure гарантирует, что данные уходят только по защищенному каналу, а вместе они позволяют безопасно использовать cookies для аутентификации и управления сессиями.
Вопрос 8. Как на стороне клиента реализовать автоматическое обновление access-токена при его истечении, если access и refresh токены передаются в HttpOnly и Secure cookies?
Таймкод: 00:10:26
Ответ собеседника: правильный. Предложил обрабатывать 401 или специальный код ошибки в интерцепторе/мидлваре, по нему вызывать запрос на refresh, затем повторять исходный запрос, учитывая, что куки браузер отправляет автоматически.
Правильный ответ:
В случае, когда access и refresh токены хранятся в HttpOnly + Secure cookies, фронтенд не имеет прямого доступа к их значениям (что является правильным решением с точки зрения безопасности). Это меняет модель: клиентская логика опирается не на чтение токена, а на семантику ответов от сервера и автоматическую отправку cookies браузером.
Общая идея:
- Access-токен короткоживущий, используется для авторизации запросов.
- Refresh-токен более долгоживущий, используется только для обновления access-токена.
- Оба токена находятся в защищённых cookies, отправляются браузером автоматически.
- Клиентский код не читает токены, а управляет потоком запросов и обработкой ошибок.
Типичный рабочий сценарий:
- Обычный запрос с истекшим access-токеном
- Клиент (SPA, мобильное приложение через WebView, etc.) отправляет запрос к защищенному API:
- Браузер автоматически добавляет HttpOnly+Secure cookies с access/refresh (если подходят по домену/Path/SameSite).
- Сервер проверяет access-токен:
- Если токен валиден → возвращает нормальный ответ.
- Если истек или некорректен → возвращает 401 Unauthorized (или кастомный код/ошибку, например
token_expired).
- Перехват 401 на клиенте
- На фронтенде используется:
- глобальный интерцептор HTTP-клиента (axios, fetch wrapper),
- мидлварь в приложении,
- единая точка обработки ошибок.
- Логика:
- Если ответ 401 (и это не запрос на refresh) — инициировать процесс обновления токена.
- Запрос на обновление токена (refresh)
- Клиент отправляет запрос на эндпоинт, например:
POST /auth/refresh
- Важные моменты:
- Никаких токенов в JS-коде не нужно явно передавать.
- Refresh-токен берется сервером из HttpOnly+Secure cookie, который браузер автоматически прикрепляет к запросу.
- Access-токен может:
- либо тоже жить в cookie и обновляться там же,
- либо возвращаться в ответе (но тогда лучше тоже сразу Set-Cookie для согласованности и безопасности).
Пример ответа сервера при успешном refresh:
HTTP/1.1 200 OK
Set-Cookie: access_token=<new>; HttpOnly; Secure; Path=/; SameSite=Lax
Content-Type: application/json
{"status":"ok"}
- Повтор исходного запроса
- Если refresh прошел успешно:
- интерцептор повторяет исходный запрос:
- уже с новым access-токеном, который автоматически отправится в cookie.
- интерцептор повторяет исходный запрос:
- Если refresh неуспешен (refresh истек или отозван):
- клиент выполняет логаут:
- очищает локальное состояние (стор),
- при необходимости перенаправляет на страницу логина.
- клиент выполняет логаут:
- Важные детали реализации
Ключевые технические аспекты:
-
Нужно предотвратить «шторм refresh-запросов»:
- Если несколько параллельных запросов получили 401, не надо отправлять N refresh-запросов.
- Правильная стратегия:
- завести глобальный «refresh in progress» флаг/промис,
- первый 401 инициирует refresh,
- остальные 401 ожидают результат одного refresh и после него либо повторяют запрос, либо делают логаут.
-
Разделять типы 401:
- 401 из-за протухшего access-токена → пробуем refresh.
- 401 после неудачного refresh или с маркером «invalid_refresh» → сразу логаут.
- Это удобнее делать, если сервер возвращает структурированное тело ответа с кодом ошибки.
-
Использовать SameSite и корректные пути:
- refresh-эндпоинт должен находиться в зоне действия refresh-cookie.
- access-cookie и refresh-cookie можно разнести по Path, если нужно больше контроля.
- Высокоуровневая схема логики на клиенте (псевдокод)
Псевдокод интерцептора:
async function httpRequest(config) {
try {
return await fetchWithConfig(config); // обычный запрос
} catch (err) {
if (err.status === 401 && !config._retry && !isAuthRefreshRequest(config)) {
// Один раз пробуем обновиться
await ensureRefreshed(); // синхронизированный вызов
config._retry = true;
return await fetchWithConfig(config); // повторяем исходный запрос
}
throw err;
}
}
let refreshPromise = null;
async function ensureRefreshed() {
if (!refreshPromise) {
refreshPromise = (async () => {
const resp = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include', // важно для отправки cookies
});
if (!resp.ok) {
// refresh не удался → логаут
handleLogout();
throw new Error('Unable to refresh token');
}
// Сервер через Set-Cookie установит новый access_token.
return true;
})();
try {
return await refreshPromise;
} finally {
refreshPromise = null;
}
}
return refreshPromise; // ждем уже запущенный refresh
}
Ключевой момент:
credentials: 'include'(или аналог в вашем HTTP-клиенте) обязателен для отправки cookies.- JS не трогает сами токены: управление исключительно через Set-Cookie / Cookie + статусы.
- Серверная сторона на Go (пример refresh-эндпоинта)
Условный пример:
http.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
// Браузер сам отправит refresh_token из HttpOnly+Secure cookie
c, err := r.Cookie("refresh_token")
if err != nil {
http.Error(w, "no refresh token", http.StatusUnauthorized)
return
}
refreshToken := c.Value
// 1. Проверяем refresh-токен: подпись, срок, отзыв и т.д.
// 2. Если валиден — генерируем новый access-токен (+ при необходимости новый refresh-токен).
newAccess := "new-access-jwt"
http.SetCookie(w, &http.Cookie{
Name: "access_token",
Value: newAccess,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
})
- Итоговые принципы
- Клиент не должен читать токены из HttpOnly cookies — это норма, а не ограничение.
- Автообновление строится вокруг:
- централизованной обработки 401,
- выделенного refresh-эндпоинта,
- автоматической отправки cookies браузером,
- повторного выполнения исходных запросов.
- Важно:
- синхронизировать refresh,
- корректно обрабатывать неуспешный refresh,
- держать безопасную конфигурацию cookies (HttpOnly, Secure, SameSite).
Вопрос 9. Как сделать так, чтобы refresh-cookie отправлялась не во все запросы, а только при обращении к определённому эндпоинту?
Таймкод: 00:12:01
Ответ собеседника: правильный. Предложил использовать атрибут path у cookie, чтобы ограничить её отправку только запросами на нужный путь.
Правильный ответ:
Для того чтобы браузер не отправлял refresh-cookie во все запросы подряд, а только при обращении к конкретному эндпоинту (например, /auth/refresh), используется атрибут Path при установке cookie.
Ключевая идея:
- Браузер добавляет cookie к запросу только если:
- домен совпадает с правилом
Domain, - URL запроса начинается с значения
Path, - соблюдены другие условия (Secure, SameSite и т.п.).
- домен совпадает с правилом
- Значит, если задать
Path=/auth/refresh, cookie будет автоматически отправляться только при запросах к:/auth/refresh- и путям, начинающимся с этого префикса (например
/auth/refresh/rotate), и не будет уходить к/api/...,/payments/...и т.д.
Практическая мотивация:
- Уменьшить поверхность атаки: refresh-токен более чувствителен, чем access-токен.
- Не светить refresh-cookie во всех запросах, логах прокси, сторонних интеграциях.
- Явно отделить использование refresh-токена только для механизма обновления сессии.
Пример установки refresh-cookie в Go:
http.HandleFunc("/auth/login", func(w http.ResponseWriter, r *http.Request) {
// После успешной аутентификации
refreshToken := "refresh-token-value"
http.SetCookie(w, &http.Cookie{
Name: "refresh_token",
Value: refreshToken,
Path: "/auth/refresh", // критично: ограничиваем область
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
// Access-токен можно выдать более широко:
http.SetCookie(w, &http.Cookie{
Name: "access_token",
Value: "access-token-value",
Path: "/", // используется в основном API
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
w.Write([]byte("OK"))
})
Теперь:
- Запрос к
/auth/refresh:- получит
refresh_token(иaccess_token, если Path подходит).
- получит
- Запрос к
/api/payments:- не получит
refresh_token, потому что его Path не матчится. - будет использовать только
access_token.
- не получит
Таким образом, за счет корректной настройки Path мы явно ограничиваем контекст использования refresh-cookie, сохраняя удобство (автоматическая отправка браузером) и повышая безопасность.
Вопрос 10. Что обозначает атрибут SameSite у cookies?
Таймкод: 00:12:53
Ответ собеседника: неправильный. Путает SameSite с перечнем доменов для установки cookie, не раскрывает реальное назначение атрибута.
Правильный ответ:
Атрибут SameSite управляет тем, будут ли cookies отправляться браузером в кросс-сайтовых запросах. Его основная цель — защита от CSRF-атак и снижение утечек cookies при взаимодействии между разными сайтами/доменами.
Важно понять:
- «SameSite» — это про контекст запроса (same-site vs cross-site),
- а не про список доменов, как в
Domain.
Контекст:
- same-site: запрос инициирован страницей с тем же «site» (регистрантский домен + public suffix), например:
- страница:
app.example.com, запрос:api.example.com→ same-site.
- страница:
- cross-site: запрос иницирован со страницы другого сайта:
- страница:
evil.com, запрос:bank.example.com→ cross-site.
- страница:
Основные значения SameSite
SameSite=Strict
- Cookie отправляется только при same-site запросах.
- Не отправляется, если пользователь переходит с другого сайта:
- по ссылке,
- через форму,
- через автозагрузку ресурсов.
- Максимальная защита от CSRF.
- Минусы:
- Может ломать сценарии, когда пользователь переходит с внешнего сайта и ожидается авторизованное состояние.
Пример:
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict
SameSite=Lax
- Более мягкий и практичный режим (современный безопасный дефолт).
- Cookie:
- отправляется при:
- переходах по ссылке (top-level GET навигация),
- прямом вводе URL,
- обновлении страницы,
- same-site запросах.
- не отправляется в большинстве фоновых cross-site запросов:
- формы POST с других сайтов,
- изображения, fetch/XHR, iframes и т.п.
- отправляется при:
- Защищает от типичных CSRF-сценариев, не ломая обычные переходы по ссылкам.
Пример:
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax
SameSite=None
- Означает: cookie можно отправлять и в cross-site запросах.
- Обязательно должно сопровождаться
Secure:- без
Secureсовременные браузеры такие cookies игнорируют.
- без
- Используется для:
- SSO между доменами,
- embedded-виджетов,
- внешних интеграций,
- если cookie нужно отправлять внутри iframe или при запросах с других доменов.
Пример:
Set-Cookie: auth_token=abc123; HttpOnly; Secure; SameSite=None
Поведение по умолчанию (важно для продакшена)
Современные браузеры (Chrome, Firefox, Safari) по умолчанию:
- если
SameSiteне указан:- трактуют cookie как
SameSite=Lax(или эквивалент), а не как «шлём везде».
- трактуют cookie как
- Это значит:
- критичные аутентификационные cookies без явного SameSite могут начать работать не так, как вы ожидали в старых моделях.
Роль SameSite в безопасности
Основная задача — защита от CSRF:
- В классическом CSRF злоумышленник с другого домена заставляет браузер жертвы отправить запрос к
bank.example.comс ее cookies. - При
SameSite=Lax/Strictбраузер может не отправить сессионные cookies в таком кросс-сайтовом сценарии, тем самым ломая атаку. - В реальных системах:
- для session-id и auth-cookie часто используют:
SameSite=LaxилиSameSite=Strict,- в комбинации с
HttpOnlyиSecure.
- для сценариев SSO/3rd-party —
SameSite=None; Secure.
- для session-id и auth-cookie часто используют:
Примеры конфигурации для аутентификации
- Классическая auth-cookie:
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
- Refresh-токен только для спец-эндпоинта:
Set-Cookie: refresh_token=def456; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh
В связке:
SameSiteконтролирует, в каких контекстах кука вообще уходит.PathиDomain— где она применима.Secure— только по HTTPS.HttpOnly— не доступна из JS.
Кратко:
SameSite— механизм ограничения отправки cookies в кросс-сайтовых запросах.- Нужен для защиты от CSRF и утечек сессий.
- Значения:
Strict— максимум защиты, минимум удобства,Lax— разумный безопасный дефолт,None— разрешаем cross-site, но только сSecure.
Вопрос 11. Как проверить, содержат ли два массива одинаковые числа с одинаковой кратностью?
Таймкод: 00:14:13
Ответ собеседника: правильный. Предложил:
- если длины массивов различаются — сразу false;
- пройти по первому массиву и подсчитать элементы в map;
- пройти по второму массиву, уменьшая счетчики, при отсутствии ключа или отрицательном значении — false;
- в конце убедиться, что все счетчики обнулились — true.
Правильный ответ:
Задача: определить, являются ли два массива мультимножествами, содержащими идентичные элементы с одинаковой кратностью (порядок не важен).
Оптимальное решение по времени O(n) и памяти O(n) — использовать хеш-таблицу (map) для подсчета количества вхождений.
Алгоритм:
- Если длины массивов разные — сразу вернуть false.
- Создаем map: значение → количество вхождений из первого массива.
- Итерируем второй массив:
- Для каждого элемента:
- если его нет в map — вернуть false;
- уменьшаем счетчик;
- если счетчик стал < 0 — вернуть false (во втором массиве этого значения больше).
- Для каждого элемента:
- В конце (опционально, но корректно и явно):
- Проверяем, что все счетчики равны 0.
- Если да — массивы эквивалентны по составу и кратности.
Почему это корректно:
- Равная длина гарантирует одинаковое общее количество элементов.
- map фиксирует, сколько раз каждое значение должно встретиться.
- Проход по второму массиву проверяет, что ни одного элемента нет «лишнего» или «нехватающего».
- Ненулевой счетчик в конце означает несовпадение кратностей.
Пример реализации на Go:
package main
import "fmt"
func equalMultiset(a, b []int) bool {
if len(a) != len(b) {
return false
}
counts := make(map[int]int, len(a))
for _, v := range a {
counts[v]++
}
for _, v := range b {
if counts[v] == 0 {
// либо элемента не было, либо уже "израсходовали"
return false
}
counts[v]--
}
// Дополнительная проверка (часто можно опустить, т.к. длины равны и мы не допускаем "минусов"):
for _, c := range counts {
if c != 0 {
return false
}
}
return true
}
func main() {
fmt.Println(equalMultiset([]int{1, 2, 2, 3}, []int{2, 1, 3, 2})) // true
fmt.Println(equalMultiset([]int{1, 2, 2}, []int{1, 2, 3})) // false
fmt.Println(equalMultiset([]int{1, 1, 2}, []int{1, 2, 2})) // false
}
Альтернативы:
- Сортировка обоих массивов и посимвольное сравнение:
- время O(n log n), память O(1)/O(n) в зависимости от реализации.
- Подходит, если нет желания или возможности использовать map, но менее эффективно для больших данных.
Такой подход — базовый и ожидаемый для задач на проверку мультимножеств, и хорошо масштабируется.
