Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Backend · Уровень: Middle+/Senior ## TL;DR - **JWT — это НЕ шифрование, а подпись.** Формат `header.payload.signature`, каждая часть — `base64url`. Payload читается кем угодно (`base64url -d`). Никогда не кладите в payload секреты (пароли, PAN-карты, токены доступа к другим системам). - **Подпись** гарантирует целостность и аутентичность, а не конфиденциальность. Для конфиденциальности нужен JWE (encrypted), а не JWS (signed) — и в 99% случаев это не то, что вам нужно. - **HS256** (HMAC) — симметричный, один секрет для подписи и проверки. **RS256/ES256** (RSA/ECDSA) — асимметричный: приватным подписывают (auth-server), публичным проверяют (resource-servers). Микросервисы → почти всегда асимметрика + JWKS. - **Валидация обязана**: проверять подпись, `exp`/`nbf`/`iat`, `iss`, `aud`, и **whitelisting алгоритма** (защита от `alg=none` и algorithm confusion). - **JWT stateless ⇒ отозвать нельзя** без внешнего состояния. Решения: короткий TTL access-токена + refresh-токен с ротацией и reuse-detection, либо denylist (Redis) / token version в БД. - **Хранение на клиенте**: `httpOnly`+`Secure`+`SameSite` cookie, а не `localStorage` (XSS читает localStorage тривиально). --- ## Теория ### 1. Структура JWT (а точнее — самая частая его форма, JWS Compact Serialization) состоит из трёх частей, разделённых точками: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MTg0MDAwMDB9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk └──────────── header ────────────────┘ └──────────── payload ──────────┘ └──────────── signature ──────────────┘ ``` Каждая часть — это **base64url** (НЕ обычный base64: `+`→`-`, `/`→`_`, padding `=` убран). Это кодирование, а не шифрование. Любой может декодировать header и payload: ```bash echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | base64 -d # {"alg":"HS256","typ":"JWT"} ``` Подпись считается над строкой `base64url(header) + "." + base64url(payload)`: ``` signature = HMAC-SHA256( base64url(header) + "." + base64url(payload), secret ) # для HS256 signature = RSASSA-PKCS1-v1_5( SHA256(...), privateKey ) # для RS256 ``` Важное следствие: подпись покрывает header и payload как **сырые base64url-строки**. Поэтому при проверке нельзя ре-сериализовать JSON и подписывать заново — нужно брать ровно те байты, что пришли. #### Header | Поле | Назначение | |------|-----------| | `alg` | Алгоритм подписи (`HS256`, `RS256`, `ES256`, `none`, ...) | | `typ` | Тип токена, обычно `JWT`. Для защиты от подмены контекста используют `at+jwt` (access token, RFC 9068) | | `kid` | Key ID — какой ключ использовать для проверки (важно при ротации ключей и JWKS) | ```json { "alg": "RS256", "typ": "JWT", "kid": "2024-q1-key" } ``` #### Payload (claims) **Registered claims** (RFC 7519, стандартизированы): | Claim | Полное имя | Смысл | |-------|-----------|-------| | `iss` | issuer | Кто выпустил токен | | `sub` | subject | Кому принадлежит (обычно user id) | | `aud` | audience | Для кого предназначен (какой сервис/API должен принимать) | | `exp` | expiration time | Unix timestamp, после которого токен невалиден | | `nbf` | not before | Не валиден ДО этого времени | | `iat` | issued at | Когда выпущен | | `jti` | JWT ID | Уникальный id токена (для denylist / защиты от replay) | **Public claims** — зарегистрированные в IANA или с collision-resistant именами (URI), например `https://example.com/roles`. **Private claims** — произвольные, по договорённости сторон (`user_id`, `tenant`, `scope`, `role`). ```json { "iss": "https://auth.example.com", "sub": "user-42", "aud": "https://api.example.com", "exp": 1718400000, "iat": 1718396400, "nbf": 1718396400, "jti": "f1d2...", "scope": "read:orders write:orders", "role": "admin" } ``` > Размер имеет значение: токен ходит в каждом запросе (заголовок `Authorization: Bearer ...` или cookie). Не пихайте в claims весь профиль пользователя — есть лимиты на размер заголовков (часто 8 KB на nginx/прокси). ### 2. Алгоритмы: HMAC vs RSA/ECDSA | | HS256 (HMAC) | RS256 (RSA) | ES256 (ECDSA) | |---|---|---|---| | Тип | Симметричный | Асимметричный | Асимметричный | | Ключ подписи / проверки | Один и тот же секрет | Приватный / Публичный | Приватный / Публичный | | Кто может проверить | Только обладатель секрета | Любой с публичным ключом | Любой с публичным ключом | | Размер подписи | 32 байта | ~256 байт (для 2048-бит) | ~64 байта | | Скорость подписи | Очень быстро | Медленно | Быстро | | Когда использовать | Монолит, один сервис подписывает и проверяет | Микросервисы, OIDC, сторонние верификаторы | То же, что RS256, но компактнее/быстрее | **Ключевая идея асимметрики**: auth-сервер держит приватный ключ и подписывает. Resource-серверы (десятки микросервисов) держат только публичный ключ и проверяют. Утечка публичного ключа не позволяет подделать токен. С HS256 каждый проверяющий сервис должен знать секрет — а значит может и подделывать токены, и утечка любого из них компрометирует всю систему. #### JWKS endpoint и `kid` В асимметричной модели публичные ключи отдаются через **JWKS** (JSON Web Key Set) — стандартный endpoint (`/.well-known/jwks.json`): ```json { "keys": [ { "kty": "RSA", "kid": "2024-q1-key", "use": "sig", "alg": "RS256", "n": "0vx7agoebGcQSuu...", "e": "AQAB" } ] } ``` Resource-сервер: читает `kid` из header токена → находит соответствующий ключ в JWKS → проверяет подпись. Это позволяет **ротировать ключи без даунтайма**: новый ключ публикуется в JWKS заранее, старый держится до истечения всех выданных токенов. В Go обычно используют кэширующий JWKS-клиент (`github.com/MicahParks/keyfunc`), чтобы не дёргать endpoint на каждый запрос. ### 3. Валидация (Go, golang-jwt/jwt v5) Полная корректная валидация HS256: ```go import ( "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" ) type AppClaims struct { Role string `json:"role"` jwt.RegisteredClaims } func ParseAndValidate(tokenStr string, secret []byte) (*AppClaims, error) { claims := &AppClaims{} token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { // КРИТИЧНО: whitelisting алгоритма. // Без этой проверки возможна algorithm confusion и alg=none. if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return secret, nil }, // Дополнительная защита: разрешённые методы на уровне парсера. jwt.WithValidMethods([]string{"HS256"}), jwt.WithIssuer("https://auth.example.com"), // проверка iss jwt.WithAudience("https://api.example.com"), // проверка aud jwt.WithExpirationRequired(), // exp обязателен jwt.WithLeeway(30*time.Second), // допуск на рассинхрон часов ) if err != nil { switch { case errors.Is(err, jwt.ErrTokenExpired): return nil, errors.New("token expired") case errors.Is(err, jwt.ErrTokenSignatureInvalid): return nil, errors.New("bad signature") default: return nil, err } } if !token.Valid { return nil, errors.New("invalid token") } return claims, nil } ``` Что именно валидируется (порядок логический): 1. **Парсинг и формат** — три части, валидный base64url, валидный JSON. 2. **Алгоритм** — соответствует ожидаемому (whitelist). Делать ДО проверки подписи. 3. **Подпись** — верна для данного ключа/алгоритма. 4. **Временны́е claims** — `exp` (не истёк), `nbf` (уже наступил), `iat` (не из будущего). С `WithLeeway` для часов. 5. **`iss`** — токен от ожидаемого издателя. 6. **`aud`** — токен предназначен этому сервису. Пропуск `aud` = токен для сервиса A примут на сервисе B. > `golang-jwt/jwt` v5 проверяет `exp`/`nbf`/`iat` автоматически, если они присутствуют. Но `iss`/`aud` НЕ проверяются, если вы не передали `WithIssuer`/`WithAudience`. И `exp` НЕ обязателен по умолчанию — нужен `WithExpirationRequired()`. Для RS256 keyfunc возвращает `*rsa.PublicKey`, а whitelist — `*jwt.SigningMethodRSA` и `WithValidMethods([]string{"RS256"})`. ### 4. Expiration и время жизни - **Access token** — короткий TTL (5–15 минут). Используется в каждом запросе. Stateless: проверяется по подписи без обращения к БД. - **Refresh token** — длинный TTL (дни/недели). Используется только для получения нового access-токена. Хранится надёжно (httpOnly cookie / secure storage), желательно с серверным состоянием (можно отозвать). - **Sliding sessions** — при каждом использовании refresh-токена выдаётся новая пара, продлевая «скользящее окно». Сессия живёт, пока пользователь активен, но умирает после периода бездействия. Зачем короткий access TTL: JWT нельзя отозвать. Короткий TTL ограничивает окно эксплуатации украденного токена. Это компромисс stateless-подхода. ### 5. Refresh tokens **Поток**: ``` 1. Login → выдать access (15 мин) + refresh (30 дней, хранится в БД) 2. Access протух → клиент шлёт refresh на /token/refresh 3. Сервер валидирует refresh → выдаёт НОВУЮ пару (ротация) 4. Старый refresh инвалидируется ``` **Refresh token rotation + reuse detection** — современный стандарт (OAuth 2.0 BCP): - Каждый refresh-токен одноразовый. При использовании он помечается «использован» и выдаётся новый. - Если приходит **уже использованный** refresh-токен — это сигнал кражи (либо вор, либо легитимный клиент используют один токен). Реакция: **отозвать всю цепочку токенов** этого пользователя/сессии (token family) и потребовать повторный логин. ```go // Псевдо-логика reuse detection func Refresh(ctx context.Context, presented string) (TokenPair, error) { rt, err := store.GetRefresh(ctx, hash(presented)) if err != nil { return TokenPair{}, ErrUnknownToken } if rt.Used { // Токен уже использовался → reuse detected → компрометация цепочки. store.RevokeFamily(ctx, rt.FamilyID) return TokenPair{}, ErrTokenReuse } store.MarkUsed(ctx, rt.ID) newPair := issuePair(rt.UserID, rt.FamilyID) store.SaveRefresh(ctx, hash(newPair.Refresh), rt.FamilyID) return newPair, nil } ``` **Хранение refresh-токенов на сервере**: храните **хэш** (SHA-256), а не сам токен — как пароли. При утечке БД злоумышленник не получит рабочие токены. ### 6. Уязвимости #### alg=none Спецификация JWS определяет `alg: "none"` (unsecured JWT — без подписи). Если библиотека или код принимает `none`, злоумышленник убирает подпись и подставляет любой payload: ```json {"alg":"none","typ":"JWT"}.{"sub":"admin"}. ``` Защита: **никогда не доверяйте `alg` из header**. Явно задавайте ожидаемый алгоритм (`WithValidMethods`, проверка типа `t.Method`). golang-jwt v5 не поддерживает `none` без явного `jwt.UnsafeAllowNoneSignatureType` — но всё равно ставьте whitelist. #### Algorithm confusion (RS256 → HS256) Классическая атака. Сервис ожидает RS256 и при верификации использует RSA-публичный ключ. Если код выбирает алгоритм по `alg` из токена, атакующий: 1. Меняет `alg` на `HS256`. 2. Подписывает токен по HMAC, **используя публичный RSA-ключ как HMAC-секрет** (публичный ключ известен — он публичный!). 3. Сервер видит `HS256`, берёт «ключ для проверки» (тот же публичный ключ) как HMAC-секрет → подпись сходится. ```go // УЯЗВИМО: ключ зависит от alg из токена func keyfunc(t *jwt.Token) (interface{}, error) { return publicKey, nil // вернётся и для HS256, и для RS256 — дыра } // БЕЗОПАСНО: жёстко фиксируем семейство алгоритмов func keyfunc(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { return nil, errors.New("unexpected alg") } return publicKey, nil } ``` #### Кража токена (XSS / MITM) - **XSS** → если токен в `localStorage`/`sessionStorage`, любой инъектированный JS его прочитает (`localStorage.getItem('token')`). Bearer-токен в JS-доступном хранилище = главная цель XSS. - **MITM** → всегда HTTPS, флаг `Secure` на cookie. - Митигейшн последствий кражи: короткий TTL, refresh rotation с reuse detection, привязка к контексту (token binding / DPoP, fingerprint), denylist. #### Отсутствие проверки exp Если не валидировать `exp` (или не требовать его наличия), токен живёт вечно. В go-jwt — добавляйте `WithExpirationRequired()`. #### Weak secret (brute force HS256) HS256 безопасен ровно настолько, насколько силён секрет. Короткий/словарный секрет подбирается офлайн (есть готовые wordlists и `hashcat` mode 16500 для JWT). Секрет должен быть **высокоэнтропийным** (≥256 бит случайных байт), не словом. Поэтому для публично доступных токенов часто предпочитают асимметрику. #### Отсутствие aud Без проверки `aud` токен, выпущенный для сервиса A, примет сервис B → cross-service token replay / повышение привилегий. ### 7. Revocation (отзыв) Фундаментальная проблема: **JWT stateless — сервер не хранит состояние сессии, проверка идёт только по подписи**. Подписанный непротухший токен валиден всегда. Отозвать его «из коробки» нельзя. Варианты: | Подход | Как работает | Минусы | |--------|-------------|--------| | **Короткий TTL** | Токен сам протухнет за минуты | Окно уязвимости остаётся; не мгновенно | | **Denylist / blocklist** | `jti` отозванных токенов в Redis с TTL = остаток жизни токена | Каждый запрос → обращение в Redis (частичная потеря stateless) | | **Token version в БД** | В claims кладут `token_version`; logout/смена пароля инкрементит версию в БД; при валидации сравнивают | Поход в БД на каждый запрос (или кэш) | | **Allowlist (whitelist) сессий** | Хранят активные сессии, по сути возврат к session-store | Полностью stateful | Практичный гибрид: короткий TTL access-токенов (минимизирует нужду в denylist) + stateful refresh-токены (их отзываем мгновенно, ротация + reuse detection) + denylist в Redis только для экстренного отзыва (бан пользователя, утечка). ```go // Denylist по jti в Redis func IsRevoked(ctx context.Context, rdb *redis.Client, jti string) (bool, error) { n, err := rdb.Exists(ctx, "denylist:"+jti).Result() return n > 0, err } // При отзыве: TTL = exp - now, чтобы запись не висела вечно func Revoke(ctx context.Context, rdb *redis.Client, jti string, exp time.Time) error { return rdb.Set(ctx, "denylist:"+jti, "1", time.Until(exp)).Err() } ``` ### 8. Где хранить на клиенте | | `localStorage` | `httpOnly` cookie | |---|---|---| | Доступ из JS | Да (уязвимо к XSS) | Нет (JS не читает) | | Авто-отправка | Нет (ручной заголовок) | Да (браузер сам шлёт) | | CSRF | Не подвержен (заголовок ставится явно) | Подвержен → нужен `SameSite` + CSRF-токен | | Рекомендация | Избегать для токенов | Предпочтительно | Рекомендуемая конфигурация cookie: ``` Set-Cookie: refresh=...; HttpOnly; Secure; SameSite=Strict; Path=/auth; Max-Age=2592000 ``` - `HttpOnly` — недоступна JS (защита от XSS-кражи). - `Secure` — только по HTTPS. - `SameSite=Strict/Lax` — защита от CSRF. - `Path=/auth` — refresh-cookie шлётся только на endpoint обновления. Частый паттерн: refresh-токен в httpOnly cookie, access-токен в памяти JS (переменная, не localStorage) — он короткоживущий и не переживёт перезагрузку, что и нужно. --- ## Подводные камни / gotchas - **Payload читаем всеми.** Не кладите PII/секреты. JWT подписан, но не зашифрован. - **`alg` из header — это ввод от атакующего.** Никогда не выбирайте ключ/алгоритм проверки на основе `alg` без whitelist. - **`exp`/`aud`/`iss` не проверяются «сами собой».** В golang-jwt нужно явно включить `WithExpirationRequired`, `WithAudience`, `WithIssuer`. Дефолтное `ParseWithClaims` проверит подпись и (если присутствуют) временны́е claims, но пропустит отсутствующий `exp`. - **Рассинхрон часов** между сервисами → ложные «token not yet valid» (`nbf`/`iat`) и «expired». Используйте `WithLeeway` (30–60 с), но не больше. - **Logout с чистым JWT ничего не отзывает.** «Удалить токен на клиенте» — это не отзыв; украденная копия продолжает работать до `exp`. - **HS256 в микросервисах** = секрет у всех проверяющих = любой сервис может подделывать токены и компрометирует всю систему при утечке. Используйте асимметрику. - **Хранение refresh в БД в открытом виде** — при утечке БД все токены рабочие. Храните хэш. - **`kid` тоже ввод от атакующего** — не используйте его напрямую как путь к файлу/ключу (path traversal, SQL-инъекция в lookup ключа). - **Большие токены** (роли, permissions в claims) могут превысить лимиты заголовков прокси (8 KB) и раздувают каждый запрос. - **Сравнение подписей** должно быть constant-time (библиотеки делают это; не катайте свою проверку HMAC через `==`). --- ## Вопросы на собеседовании **В:** JWT — это шифрование или подпись? Можно ли класть в payload пароль? **О:** Стандартный JWT (JWS) — это **подпись**, а не шифрование. Header и payload закодированы base64url и читаются любым (`base64url -d`). Подпись гарантирует целостность и подлинность, но не конфиденциальность. Пароли/секреты класть нельзя. Для конфиденциальности существует JWE (шифрованный JWT), но он нужен редко. **В:** В чём разница HS256 и RS256, когда что выбирать? **О:** HS256 — HMAC, симметричный: один секрет и для подписи, и для проверки. RS256 — RSA, асимметричный: приватным подписывают, публичным проверяют. HS256 подходит для монолита, где один сервис и подписывает, и проверяет. RS256/ES256 — для микросервисов и OIDC: auth-сервер держит приватный ключ, resource-серверы — только публичный (через JWKS), не могут подделывать токены. С HS256 любой проверяющий знает секрет и потенциально может подделать токен. **В:** Что такое атака `alg=none` и как от неё защититься? **О:** Спецификация позволяет `alg: "none"` — токен без подписи. Если код принимает такой токен, атакующий подставляет произвольный payload с пустой подписью и проходит проверку. Защита — whitelist алгоритмов: явно проверять, что `alg` соответствует ожидаемому семейству (в golang-jwt — `WithValidMethods` и проверка типа `t.Method`), и никогда не доверять `alg` из header. **В:** Объясните algorithm confusion RS256→HS256. **О:** Сервис ожидает RS256 и при проверке использует RSA-публичный ключ. Если выбор алгоритма зависит от `alg` в токене, атакующий меняет `alg` на HS256 и подписывает токен HMAC'ом, используя **публичный ключ как HMAC-секрет** (он публично известен). Сервер берёт тот же публичный ключ как HMAC-секрет — подпись сходится. Защита — жёстко фиксировать допустимый алгоритм и не выбирать ключ на основе `alg` из токена. **В:** Можно ли отозвать JWT? Как реализовать logout? **О:** Чистый JWT stateless — отозвать нельзя, токен валиден до `exp`. Варианты: короткий TTL access-токена; denylist `jti` в Redis с TTL = остаток жизни; `token_version` в claims, сравниваемая с БД; stateful refresh-токены, которые отзываются мгновенно. Logout обычно = отзыв refresh-токена + (при необходимости) добавление access `jti` в denylist. Удаление токена только на клиенте отзывом не является. **В:** Зачем нужны refresh-токены и что такое их ротация с reuse detection? **О:** Access-токен короткоживущий (минуты) ради безопасности, refresh — долгоживущий, для получения новых access без повторного логина. Ротация: каждый refresh одноразовый, при использовании выдаётся новый, старый инвалидируется. Reuse detection: если приходит уже использованный refresh — это признак кражи, и вся цепочка токенов (token family) этого пользователя отзывается с требованием перелогина. **В:** Где хранить токены на клиенте и почему не в localStorage? **О:** `localStorage` доступен любому JS, поэтому при XSS токен тривиально крадётся. Лучше httpOnly + Secure + SameSite cookie: JS её не читает (защита от XSS-кражи), браузер шлёт автоматически. Минус cookie — CSRF, решается `SameSite` и/или CSRF-токеном. Частый паттерн: refresh в httpOnly cookie, короткоживущий access — в памяти JS. **В:** Какие claims обязательно проверять при валидации и почему? **О:** Подпись + алгоритм (whitelist), `exp` (не истёк, и желательно требовать его наличие), `nbf`/`iat` (с leeway на часы), `iss` (ожидаемый издатель), `aud` (токен предназначен именно этому сервису). Пропуск `aud` ⇒ токен сервиса A примет сервис B (cross-service replay). Пропуск `exp` ⇒ вечный токен. В golang-jwt v5 `iss`/`aud` и обязательность `exp` надо включать явно. **В:** Почему HS256 с коротким секретом опасен? **О:** Безопасность HMAC целиком зависит от секрета. Слабый/словарный секрет подбирается офлайн (hashcat, mode 16500): достаточно одного перехваченного токена. Секрет должен быть ≥256 бит случайных байт. Это одна из причин предпочитать асимметрику в публичных системах — там утечка публичного ключа не даёт подделывать токены. **В:** Что такое JWKS и зачем `kid`? **О:** JWKS — JSON Web Key Set, endpoint (`/.well-known/jwks.json`) с публичными ключами издателя. `kid` в header токена указывает, каким ключом проверять подпись. Это позволяет ротировать ключи без даунтайма: новый ключ публикуется заранее, старый держится до истечения всех выданных токенов. Verifier кэширует JWKS, чтобы не дёргать endpoint на каждый запрос. --- ## На что копают на senior+ - **Stateless vs stateful компромисс.** Senior должен внятно проговорить, что «настоящий» отзыв требует состояния, и обосновать выбор: короткий TTL + ротация refresh покрывают 95% кейсов без денормализации в stateless; denylist добавляет точечно. Уметь оценить нагрузку на Redis/БД. - **Token family и reuse detection** — детали реализации: как связать цепочку (family_id), что именно отзывать, гонки при параллельных refresh с одного клиента (двойной запрос → ложное срабатывание reuse; решают grace-периодом или идемпотентностью). - **Algorithm confusion и доверие к header** — умение показать конкретный уязвимый keyfunc и его исправление, понимание, что `alg`/`kid` — это untrusted input. - **Ротация ключей**: overlapping keys в JWKS, как старые токены продолжают валидироваться, что делать при компрометации приватного ключа (немедленная ротация + denylist всех токенов). - **Clock skew и leeway** в распределённой системе — как влияет на `nbf`/`exp`, почему большой leeway плохо. - **Token binding / DPoP / mTLS** — как привязать токен к клиенту, чтобы украденный bearer-токен был бесполезен (sender-constrained tokens, RFC 9449). - **Где НЕ нужен JWT**: для внутренних сессий одного монолита обычная серверная сессия (opaque token + session store) часто проще, безопаснее (мгновенный отзыв) и компактнее. Senior умеет сказать «здесь JWT не нужен». - **Размер и производительность**: RS256-подпись медленнее HMAC; при высоком RPS verifier на ES256 выгоднее RSA; раздувание claims бьёт по каждому запросу. - **`typ: at+jwt` (RFC 9068)** и явное разделение access/id/refresh — чтобы id-token не приняли как access-token.