Перейти к основному содержимому

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ Javascript разработчик OZON - MIDDLE

· 33 мин. чтения

Сегодня мы разберем живое техническое интервью фронтенд-разработчика с акцентом на финтех: от требований к высоконагруженной платежной системе и работе с безопасностью до вопросов по HTTP, кукам и реализации авторизации через токены. В ходе беседы кандидат демонстрирует базовое понимание сетевого взаимодействия и принципов работы протоколов, а также решает алгоритмическую задачу, что позволяет оценить его уровень подготовки и стиль мышления на практике.

Вопрос 1. Знаком ли кандидат с информацией о финтех-направлении и вакансии, которую ранее предоставила команда подбора?

Таймкод: 00:00:45

Ответ собеседника: правильный. Кандидат подтверждает, что получил общее представление о финтех-направлении, возможностях и команде платежей.

Правильный ответ:
Да, я уже ознакомился с информацией, которую предоставила команда подбора: общим описанием финтех-направления, ключевыми продуктами, ролью платёжной команды и текущими задачами. Если потребуется, могу уточнить детали по архитектуре платежей, процессам интеграции, требованиям к надежности и соответствию регуляторике, чтобы лучше сопоставить свой опыт с задачами команды.

Вопрос 2. Что происходит в сети после ввода URL в браузере и нажатия Enter (без деталей отрисовки страницы)?

Таймкод: 00:04:15

Ответ собеседника: неполный. Описал DNS-резолвинг домена в IP, использование кешей, установку TCP-соединения, использование HTTP/HTTPS и отправку запроса на сервер.

Правильный ответ:

Процесс можно разбить на несколько логических этапов: разбор URL, определение IP-адреса, установление соединения, шифрование (для HTTPS), отправка HTTP-запроса, получение ответа. Рассмотрим подробно.

  1. Разбор URL
  • Браузер разбирает URL на части:
    • схема: http / https
    • домен: example.com
    • порт: по умолчанию 80 для HTTP, 443 для HTTPS (или указанный явно)
    • путь, query-параметры, фрагмент
  • На основе схемы выбирается протокол прикладного уровня (HTTP/HTTPS) и дальнейшее поведение (например, необходимость TLS).
  1. Проверка кешей и HSTS/редиректы
  • Перед реальным запросом могут сработать:
    • кешированные редиректы (например, ранее полученный 301)
    • HSTS (браузер знает, что с доменом можно работать только по HTTPS и сразу меняет http -> https)
    • кеш DNS записей в самом браузере.
  1. Определение IP-адреса (DNS-резолвинг) Браузер получает IP-адрес домена. Последовательность:
  • Проверка:
    • кеш браузера
    • кеш ОС
    • файл hosts (локальные переопределения)
  • Если записи нет, ОС обращается к настроенным DNS-серверам (например, 8.8.8.8 или DNS провайдера).
  • Последовательность DNS-запросов может включать:
    • рекурсивный DNS-сервер провайдера или публичный
    • корневые DNS-серверы
    • TLD-серверы (.com, .ru и т.д.)
    • авторитативный DNS-сервер домена
  • В итоге возвращается запись:
    • A/AAAA (IPv4/IPv6 адрес)
    • возможные CNAME (alias), которые доп. резолвятся
  • Результат кешируется на различных уровнях (с учётом TTL).
  1. Установление сетевого соединения (TCP) После получения IP:
  • Определяется порт (по схеме или явно из URL).
  • Устанавливается TCP-соединение (если его ещё нет и не используется keep-alive / connection pooling):
    • трехстороннее рукопожатие:
      • клиент → сервер: SYN
      • сервер → клиент: SYN+ACK
      • клиент → сервер: ACK
  • На этом этапе формируется канал передачи данных с гарантированной доставкой и упорядочиванием сегментов.
  1. TLS-рукопожатие (для HTTPS) Если используется HTTPS, поверх TCP накладывается TLS:
  • Клиент отправляет:
    • ClientHello (версии протокола, поддерживаемые шифры, поддержка ALPN, SNI — домен, к которому обращаемся).
  • Сервер отвечает:
    • ServerHello (выбор шифра и параметров),
    • сертификат сервера,
    • дополнительные параметры для обмена ключами.
  • Клиент:
    • проверяет сертификат:
      • цепочку доверия (CA),
      • срок действия,
      • домен (CN/SAN должен соответствовать),
      • статус отзыва (OCSP/CRL при необходимости).
    • генерирует и/или согласует с сервером сессионные ключи.
  • После успешного рукопожатия:
    • устанавливается зашифрованный канал.
  • Возможны оптимизации:
    • TLS session resumption,
    • 0-RTT в TLS 1.3.

Важно: если сертификат недействителен или домен не совпадает, корректный клиент должен показать предупреждение и не продолжать (если пользователь не проигнорирует вручную).

  1. Отправка 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, но логика для разработчика похожа: запрос/ответ.
  1. Обработка запроса на сервере На стороне сервера:
  • Запрос попадает на:
    • 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>
  1. Доставка ответа и завершение/переиспользование соединения
  • Ответ возвращается по тому же соединению.
  • Браузер:
    • может повторно использовать соединение (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:порт):

  1. Клиент → Сервер: SYN

    • Клиент посылает сегмент с флагом SYN и начальным номером последовательности (ISN — Initial Sequence Number).
    • Говорит: «Хочу установить соединение, мой стартовый seq = X».
  2. Сервер → Клиент: SYN+ACK

    • Сервер, если порт слушает и готов, отвечает:
      • SYN с собственным ISN = Y,
      • ACK с подтверждением получения X+1.
    • Говорит: «Я согласен, мой seq = Y, твой X получил».
  3. Клиент → Сервер: ACK

    • Клиент подтверждает получение SYN+ACK:
      • ACK с номером Y+1.
    • Соединение установлено.

После этого обе стороны считают, что:

  • имеется логический двунаправленный канал,
  • они согласовали начальные номера последовательностей,
  • могут надежно обмениваться байтовым потоком.

Поддержание соединения

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:

  1. Установление TCP-соединения (описано выше).
  2. Установление TLS-сессии (TLS Handshake).
  3. Передача 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-запрос состоит из трех основных частей:

  1. Стартовая строка (Request Line)
  2. Заголовки (Headers)
  3. Пустая строка и, опционально, тело (Body)

Рассмотрим подробно.

  1. Стартовая строка (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

Ключевой смысл:

  • Стартовая строка определяет «что делаем» и «над чем».
  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/json
      • Content-Type: application/x-www-form-urlencoded
      • Content-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

  1. Тело запроса (Body)

Тело присутствует не всегда:

  • У GET, DELETE чаще без тела (по спецификации тело допустимо, но обычно не используют).
  • У POST, PUT, PATCH почти всегда есть тело.

Типичные варианты тела:

  • JSON:
    Content-Type: application/json

    {"name": "Ivan", "age": 30}
  • Формы (HTML):
    • application/x-www-form-urlencoded
    • multipart/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

  1. С использованием стандартного клиента:
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)
}
  1. Ручная сборка «сырых» данных (понимание структуры):
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.

Основные аспекты:

  1. Семантика методов
  • GET:

    • Семантически: запросить представление ресурса.
    • Должен быть:
      • безопасным — не изменять состояние на сервере;
      • идемпотентным — повторный запрос не меняет результат с точки зрения состояния ресурса.
    • Примеры:
      • получение профиля пользователя,
      • получение списка транзакций,
      • проверка статуса платежа.
  • POST:

    • Семантически: создать подресурс или инициировать обработку/операцию, по результату которой может измениться состояние.
    • Не гарантирует идемпотентность.
    • Используется для:
      • создания ресурса (создать платеж),
      • выполнения команды (инициировать перевод),
      • сложных запросов, когда параметры неуместны в URL.
    • Если один и тот же POST отправлен дважды — это могут быть два платежа, два заказа и т.п.

Важно: спецификация не запрещает серверу делать GET с побочным эффектом или POST — идемпотентным, но это нарушение устоявшейся семантики и ломает кэширование, ретраи и инфраструктуру.

  1. Передача данных: URL vs тело
  • GET:

    • Данные обычно передаются в:
      • query-параметрах: /payments?user_id=123&status=success
      • path-параметрах: /users/123/payments
    • Тело формально не запрещено в некоторых версиях спецификаций, но:
      • на практике широко не используется,
      • многие прокси/серверы/клиенты игнорируют или не ожидают тело у GET.
    • Поэтому для предикатов поиска, фильтрации и навигации — query и путь.
  • POST:

    • Данные передаются в теле запроса:
      • application/json
      • application/x-www-form-urlencoded
      • multipart/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"
}
  1. Идемпотентность и безопасность
  • Безопасность (safe):

    • GET: безопасный — не должен менять состояние на сервере.
    • POST: небезопасный — предполагает изменения.
  • Идемпотентность:

    • GET: идемпотентен. 10 одинаковых GET-запросов должны иметь тот же эффект, что и один (с точки зрения состояния).
    • POST: неидемпотентен. Повторный запрос может создать дубль транзакции, заказа, перевода.

Это важно для:

  • ретраев запросов (клиенты, балансировщики, прокси),
  • проектирования платежных и финансовых API:
    • для создания транзакции — POST,
    • для проверки статуса — GET.

В финансовых системах из-за этого часто используют:

  • POST с idempotency key в заголовках (например, Idempotency-Key) или в теле:
    • чтобы сервер сам сделал операцию идемпотентной, даже на уровне POST.
  1. Кеширование

Реальные правила кеширования завязаны не только на метод, но и на заголовки. Однако:

  • GET:

    • Может кешироваться:
      • браузером,
      • прокси,
      • CDN.
    • По умолчанию кеширующие прокси имеют право кешировать успешные GET-ответы, если заголовки не запрещают.
    • Управление:
      • Cache-Control, Expires, ETag, Last-Modified.
    • Query-параметры:
      • используются для вариативности (фильтры, страницы, версии),
      • не делают запрос «автоматически кешируемым», но CDN/прокси обычно умеют кешировать конкретные URL целиком.
  • POST:

    • По спецификации может быть кеширован, но:
      • по умолчанию кеш не обязан его кешировать,
      • требует явных заголовков (Cache-Control и Expires) и поддержки со стороны кеширующего прокси.
    • На практике POST почти всегда рассматривается как некешируемый, особенно для операций с изменением состояния и чувствительных данных (платежи, логины).

Ключевой практический вывод:

  • GET — основной кандидат для чтения и кеширования.
  • POST — для операций, которые изменяют состояние или содержат чувствительные данные, и обычно не кешируется.
  1. URL длина и практические ограничения
  • GET:
    • URL имеет ограничения по длине в браузерах и серверах (часто от ~2К до ~8К+ байт).
    • Не подходит для очень большого объёма данных (особенно бинарных).
  • POST:
    • Подходит для больших payload’ов (файлы, большие JSON и т.п.), ограничения задаются сервером (например, client_max_body_size в Nginx).
  1. Примеры на 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)
}
  1. Типичные ошибки и то, чего стоит избегать
  • Использовать 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:

  1. Установка 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>
  1. Отправка 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

  1. Идентификация сессий

Частый сценарий:

  • Пользователь логинится (логин/пароль, SSO и т.п.).
  • Сервер создает сессионную запись в БД/хранилище:
    • session_id -> user_id, роль, контекст.
  • Отправляет клиенту:
Set-Cookie: session_id=abc123; HttpOnly; Secure; Path=/; SameSite=Lax
  • При следующем запросе браузер добавляет:
Cookie: session_id=abc123
  • Сервер по session_id находит пользователя и понимает, кто делает запрос.

Это базовый механизм авторизации в веб-приложениях.

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

Примеры:

  • Тема интерфейса:
    • theme=dark
  • Выбранный язык:
    • lang=ru
  • AB-тесты, предпочтения фильтров и т.п.

Тут данные могут быть не чувствительны, часто могут читаться JavaScript’ом.

  1. Трекинг и аналитика
  • Cookie могут использоваться для:
    • идентификации уникальных пользователей,
    • сквозной аналитики (user_id/anonymous_id),
    • рекламных меток (3rd party cookies).
  • Современные браузеры и политики приватности (Safari, Firefox, Chrome) постепенно ужесточают правила для third-party cookies.
  1. Безопасность и антифрод

В финтех и чувствительных системах:

  • Cookies могут содержать:
    • session_id с привязкой к устройству/IP/fingerprint,
    • маркеры для доп. валидации (например, риск-оценка, device-id).
  • Важно:
    • не класть в cookie секреты в открытом виде (ключи, ПИНы, полные токены без шифрования),
    • использовать HttpOnly + Secure + SameSite.

Типичные практики и анти-паттерны

  • Хорошие практики:

    • session cookie:
      • HttpOnly,
      • Secure,
      • SameSite=Lax или Strict для защиты от CSRF.
    • Шифровать или подписывать значения, чтобы их нельзя было подделать.
    • Минимизировать объем и число cookies (каждый запрос несет их в заголовках).
  • Плохие практики:

    • класть в 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 тесты, трекинг).

Кратко про ключевые атрибуты, упомянутые в вопросе.

  1. Атрибут path
  • Определяет, для какого префикса пути URL cookie будет отправляться.
  • Примеры:
    • Path=/ — отправляется для всех путей на домене.
    • Path=/api — отправляется только для /api/..., но не для /admin/....
  • Практическое применение:
    • Ограничение области видимости cookies между разными приложениями/подсистемами.
    • Разделение сессий/настроек по зонам приложения.
  • Это уменьшает утечку лишних данных и немного снижает поверхность атак.
  1. Атрибут HttpOnly
  • Если установлен, cookie недоступен из JavaScript (document.cookie):
    • браузер продолжает автоматически отправлять его на сервер,
    • но JS-код не может прочитать или модифицировать его.
  • Практическое применение:
    • Использование для сессионных идентификаторов и токенов аутентификации.
    • Минимизация риска кражи cookie через XSS:
      • даже если злоумышленник внедрит скрипт, прочитать HttpOnly-cookie он не сможет.
  • Обязательный атрибут для всех чувствительных cookies (session_id, auth-token).
  1. Атрибут Secure
  • Если установлен, cookie отправляется только по HTTPS:
    • не будет отправлен по незашифрованному HTTP даже на тот же домен.
  • Практическое применение:
    • Защита от утечки сессионных токенов в открытом трафике.
    • В продакшене для auth-cookie и любых чувствительных данных должен использоваться всегда.
  • В связке с HttpOnly и SameSite формирует базовый уровень защиты для аутентификации.
  1. Практическое использование 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) — критична для платежных и любых чувствительных систем.
  1. Пример установки 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, отправляются браузером автоматически.
  • Клиентский код не читает токены, а управляет потоком запросов и обработкой ошибок.

Типичный рабочий сценарий:

  1. Обычный запрос с истекшим access-токеном
  • Клиент (SPA, мобильное приложение через WebView, etc.) отправляет запрос к защищенному API:
    • Браузер автоматически добавляет HttpOnly+Secure cookies с access/refresh (если подходят по домену/Path/SameSite).
  • Сервер проверяет access-токен:
    • Если токен валиден → возвращает нормальный ответ.
    • Если истек или некорректен → возвращает 401 Unauthorized (или кастомный код/ошибку, например token_expired).
  1. Перехват 401 на клиенте
  • На фронтенде используется:
    • глобальный интерцептор HTTP-клиента (axios, fetch wrapper),
    • мидлварь в приложении,
    • единая точка обработки ошибок.
  • Логика:
    • Если ответ 401 (и это не запрос на refresh) — инициировать процесс обновления токена.
  1. Запрос на обновление токена (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"}
  1. Повтор исходного запроса
  • Если refresh прошел успешно:
    • интерцептор повторяет исходный запрос:
      • уже с новым access-токеном, который автоматически отправится в cookie.
  • Если refresh неуспешен (refresh истек или отозван):
    • клиент выполняет логаут:
      • очищает локальное состояние (стор),
      • при необходимости перенаправляет на страницу логина.
  1. Важные детали реализации

Ключевые технические аспекты:

  • Нужно предотвратить «шторм 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, если нужно больше контроля.
  1. Высокоуровневая схема логики на клиенте (псевдокод)

Псевдокод интерцептора:

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 + статусы.
  1. Серверная сторона на 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"}`))
})
  1. Итоговые принципы
  • Клиент не должен читать токены из HttpOnly cookies — это норма, а не ограничение.
  • Автообновление строится вокруг:
    • централизованной обработки 401,
    • выделенного refresh-эндпоинта,
    • автоматической отправки cookies браузером,
    • повторного выполнения исходных запросов.
  • Важно:
    • синхронизировать refresh,
    • корректно обрабатывать неуспешный refresh,
    • держать безопасную конфигурацию cookies (HttpOnly, Secure, SameSite).

Таймкод: 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_tokenaccess_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

  1. SameSite=Strict
  • Cookie отправляется только при same-site запросах.
  • Не отправляется, если пользователь переходит с другого сайта:
    • по ссылке,
    • через форму,
    • через автозагрузку ресурсов.
  • Максимальная защита от CSRF.
  • Минусы:
    • Может ломать сценарии, когда пользователь переходит с внешнего сайта и ожидается авторизованное состояние.

Пример:

Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict
  1. 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
  1. 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 (или эквивалент), а не как «шлём везде».
  • Это значит:
    • критичные аутентификационные 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.

Примеры конфигурации для аутентификации

  • Классическая 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) для подсчета количества вхождений.

Алгоритм:

  1. Если длины массивов разные — сразу вернуть false.
  2. Создаем map: значение → количество вхождений из первого массива.
  3. Итерируем второй массив:
    • Для каждого элемента:
      • если его нет в map — вернуть false;
      • уменьшаем счетчик;
      • если счетчик стал < 0 — вернуть false (во втором массиве этого значения больше).
  4. В конце (опционально, но корректно и явно):
    • Проверяем, что все счетчики равны 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, но менее эффективно для больших данных.

Такой подход — базовый и ожидаемый для задач на проверку мультимножеств, и хорошо масштабируется.