API

Безпека API, кешування та інтернаціоналізація

TLS, UUID як ідентифікатори, JWT-безпека, Cache-Control, E-Tag-стратегії, rate limiting, інтернаціоналізація та локалізація відповідей.

Безпека API, кешування та інтернаціоналізація

API — це публічний інтерфейс вашої системи. Кожен ендпоінт — потенційна вхідна точка для зловмисника. У цій статті ми розглянемо ключові аспекти захисту API, правильне кешування та підготовку до міжнародного використання.

1. TLS: перше правило безпеки

TLS (Transport Layer Security) — це протокол шифрування, що захищає дані під час передачі. Для API це означає: завжди https://, ніколи http://.

Навіщо потрібен TLS

Без TLS дані між клієнтом і сервером передаються відкритим текстом. Будь-хто, хто «сидить» між ними (провайдер, публічна Wi-Fi мережа, зловмисник), може:

ЗагрозаПриклад
Перехоплення (sniffing)Зловмисник читає JWT-токени та паролі
Підміна (man-in-the-middle)Зловмисник змінює відповідь сервера
Повтор (replay attack)Зловмисник перехоплює запит і надсилає його повторно

TLS шифрує весь трафік між клієнтом і сервером. Навіть якщо зловмисник перехопить пакети — він побачить лише шифрований потік байтів.

Правило без винятків: HTTP без TLS (http://) допустимий тільки для локальної розробки (localhost). Будь-який продакшн API повинен працювати виключно через HTTPS. Сучасні браузери навіть маркують HTTP-сайти як «небезпечні».

2. UUID замість послідовних ID

Проблема послідовних ID

Якщо ваш API використовує послідовні числові ідентифікатори (/v1/orders/42, /v1/orders/43), зловмисник може:

  1. Перебрати ID — запитати /v1/orders/1, /v1/orders/2, ..., /v1/orders/100000 і зібрати всі дані
  2. Оцінити масштаб — якщо останнє замовлення має ID 50 000, зловмисник знає, що у вас ~50 000 замовлень
  3. Вгадати ресурс — якщо створив замовлення #42, то замовлення #41 належить іншому користувачу

UUID як рішення

UUID (Universally Unique Identifier) — 128-бітний ідентифікатор, що генерується випадково:

/v1/orders/550e8400-e29b-41d4-a716-446655440000

UUID неможливо перебрати (простір 2¹²⁸ ≈ 3.4 × 10³⁸ варіантів) і він не розкриває інформацію про кількість записів або порядок створення.

UUID як ідентифікатор
app.MapPost("/v1/orders", (OrderRequest req) =>
{
    var order = new Order
    {
        // UUID замість auto-increment
        Id = Guid.NewGuid(),
        Recipe = req.Recipe,
        CreatedAt = DateTime.UtcNow
    };
    
    db.Save(order);
    return Results.Created(
        $"/v1/orders/{order.Id}", order);
});
Коли послідовні ID допустимі: Всередині мікросервісів (де нема зовнішнього доступу), для технічних таблиць, або коли перебір ID захищений авторизацією на рівні ресурсу. Але для зовнішніх URL API завжди віддавайте перевагу UUID.

3. JWT: безпека на практиці

Ми вже розглядали JWT як механізм stateless-авторизації у статті 05. Тепер зосередимось на безпекових аспектах.

Що зберігати в JWT

JWT-токен — це не сховище даних. Він підписаний, але не зашифрований — будь-хто може декодувати його через Base64. Тому:

✅ Можна зберігати❌ Не можна зберігати
user_id (ідентифікатор)Пароль
roles (ролі)Номер кредитної картки
exp (час закінчення)Медичні дані
iat (час створення)Персональні дані (GDPR)

Час життя токена

Чим коротший час життя токена — тим безпечніше, але і менш зручно для користувача:

Тип токенаЧас життяПризначення
Access token15-60 хвилинДля запитів до API
Refresh token7-30 днівДля отримання нового access token

Чому два токени? Access token — короткоживучий, бо він передається у кожному запиті (більший ризик перехоплення). Refresh token — довгоживучий, використовується рідко (тільки для оновлення), і його можна відкликати на сервері.

Що робити при компрометації

Якщо access token викрадено, зловмисник може використовувати його до закінчення терміну дії. Зменшити ризик:

  1. Короткий час життя — через 15 хвилин токен просто перестане працювати
  2. Перевірка IP / fingerprint — токен прив'язаний до клієнта
  3. Blacklist — для критичних систем: список відкликаних токенів (порушує stateless, але іноді необхідно)

4. Cache-Control: правильне кешування

Кешування — один із принципів REST, і HTTP надає потужний механізм для його контролю через заголовок Cache-Control.

Два типи кешування

ТипДе зберігається кешХто контролює
КлієнтськийБраузер, мобільний додатокЗаголовки відповіді сервера
ПроміжнийCDN, проксі-серверЗаголовки відповіді сервера

Основні директиви Cache-Control

ДирективаОписПриклад використання
no-storeНе кешувати взагаліПерсональні дані, помилки
no-cacheКешувати, але перевіряти актуальністьПрофіль користувача
max-age=NКешувати N секунд без перевіркиСтатичні ресурси
privateКешувати тільки на клієнтіПерсональні дані
publicКешувати скрізь (включно з CDN)Публічні списки
must-revalidateПісля закінчення max-age — обов'язково перевіритиКритичні дані

Стратегії кешування для різних ресурсів

РесурсCache-ControlЧому
Список рецептів кавиpublic, max-age=3600Рідко змінюється, публічний
Профіль користувачаprivate, no-cacheПерсональний, може змінитися
Активне замовленняprivate, no-storeЧасто змінюється, кешувати небезпечно
Помилка 4xxno-storeПомилку кешувати не можна
Зображення логотипуpublic, max-age=86400Майже ніколи не змінюється

ETag + Cache-Control — комбінований підхід

Найефективніша стратегія кешування — комбінування Cache-Control з ETag:

  1. Сервер повертає Cache-Control: max-age=300 і ETag: "v1"
  2. Протягом 5 хвилин клієнт використовує локальний кеш (навіть не звертається до сервера)
  3. Після 5 хвилин клієнт робить умовний запит з If-None-Match: "v1"
  4. Якщо дані не змінились → 304 Not Modified (сервер не передає тіло)
  5. Якщо змінились → 200 OK з новими даними і новим ETag

Це дає максимальну ефективність: нема зайвих запитів (перші 5 хвилин), нема зайвих даних (304 після 5 хвилин).

Комбіноване кешування
app.MapGet("/v1/recipes", (HttpContext ctx) =>
{
    var recipes = db.GetAllRecipes();
    var etag = $"\"{recipes.GetHashCode()}\"";
    
    // Умовний запит — перевірка ETag
    var ifNoneMatch = ctx.Request.Headers
        .IfNoneMatch.FirstOrDefault();
    if (ifNoneMatch == etag)
        return Results.StatusCode(304);
    
    // Заголовки кешування
    ctx.Response.Headers.ETag = etag;
    ctx.Response.Headers.CacheControl = 
        "public, max-age=3600"; // 1 година
    
    return Results.Ok(recipes);
});
Пастка з книги: деякі 4xx статус-коди кешуються за замовчуванням — зокрема 404, 405, 410, 414. Це може призвести до ситуації, коли клієнт кешує помилку і продовжує бачити 404 навіть після того, як ресурс з'явився. Рішення: завжди явно вказуйте Cache-Control: no-store для помилок.

5. Інтернаціоналізація (i18n)

Якщо ваш API обслуговує клієнтів з різних країн, потрібно враховувати мовні та культурні відмінності.

Заголовок Accept-Language

Клієнт повідомляє бажану мову через стандартний HTTP-заголовок:

GET /v1/products/42 HTTP/1.1
Accept-Language: uk-UA, en-US;q=0.5

Це означає: «Я хочу українську (пріоритет 1.0). Якщо немає — англійську (пріоритет 0.5)».

Що локалізувати

ЩоПотрібно локалізувати?Приклад
Назви продуктів✅ Так«Лунго» / «Lungo»
Повідомлення помилок✅ Так (localized_message)«Невалідний запит» / «Invalid request»
Дати⚠️ ЗалежитьISO 8601 — універсальний, локалізація — на клієнті
Грошові суми❌ НіЗавжди в мінімальних одиницях або рядках
Імена полів JSON❌ НіЗавжди англійською: "recipe", не "рецепт"

Стратегія: контент vs метадані

Є два підходи до локалізації:

{
  "id": 42,
  "name": "Лунго",
  "description": "Подовжений еспресо з м'яким смаком"
}

Сервер сам обирає мову на основі Accept-Language. Простіше для клієнта, але ускладнює кешування (різні мови = різний кеш).

Практична рекомендація: Для більшості API перший підхід (серверна локалізація) зручніший. Якщо кешування критичне — додайте мову до ключа кешу: Vary: Accept-Language.

Часові зони

Окреме джерело проблем — часові зони. Правила:

  1. Зберігайте усі дати в UTC на сервері
  2. Передавайте дати у форматі ISO 8601 з вказанням зони: "2024-02-26T14:30:00Z" (Z = UTC)
  3. Локалізацію часових зон залиште клієнту — мобільний додаток знає часову зону користувача

Ніколи не зберігайте дати у «локальному» часі на сервері. «14:00 у Києві» і «14:00 у Лондоні» — це різні моменти часу. Якщо сервер зберігає лише «14:00» без зони — ніхто не знає, що це означає.


6. Додаткові заходи безпеки

CORS (Cross-Origin Resource Sharing)

Якщо ваш API використовується з браузера (SPA-додатки), потрібно налаштувати CORS — механізм, що контролює, з яких доменів дозволені запити:

CORS у ASP.NET Core
builder.Services.AddCors(options =>
{
    options.AddPolicy("api", policy =>
    {
        policy
            // Дозволені домени
            .WithOrigins(
                "https://app.coffee-service.com",
                "https://admin.coffee-service.com")
            // Дозволені методи
            .WithMethods("GET", "POST", "PUT", "DELETE")
            // Дозволені заголовки
            .WithHeaders("Authorization", "Content-Type",
                "If-Match", "If-None-Match")
            // Дозволити cookies/credentials
            .AllowCredentials();
    });
});

app.UseCors("api");

Rate limiting за категоріями

Не всі ендпоінти потребують однакових лімітів:

КатегоріяЛімітЧому
Читання (GET)1000 req/minБезпечні, кешовані
Запис (POST/PUT)100 req/minМодифікують стан
Аутентифікація10 req/minЗахист від brute-force
Пошук30 req/minРесурсомісткі операції

Заголовки безпеки

Заголовки безпеки
app.Use(async (ctx, next) =>
{
    // Заборона відображення сторінки в iframe
    ctx.Response.Headers["X-Frame-Options"] = "DENY";
    
    // Блокувати MIME-sniffing
    ctx.Response.Headers["X-Content-Type-Options"] = 
        "nosniff";
    
    // Суворий TLS
    ctx.Response.Headers
        ["Strict-Transport-Security"] = 
        "max-age=31536000; includeSubDomains";
    
    await next(ctx);
});

7. Практичні завдання

Рівень 1: Базовий

Рівень 2: Проєктування


8. Резюме

TLS — обов'язковий

Тільки HTTPS у продакшені. HTTP без шифрування — лише для localhost. Без TLS токени, паролі та дані передаються відкритим текстом.

UUID замість integer

Послідовні ID розкривають інформацію та дозволяють перебір. UUID неможливо вгадати — 2¹²⁸ варіантів.

Cache-Control + ETag

max-age для «гарячих» даних, ETag для умовних запитів. Комбінація дає мінімум трафіку і максимум швидкості.

i18n = UTC + Accept-Language

Дати завжди в UTC. Мова через Accept-Language. Локалізацію часових зон залишайте клієнту.

Далі: у фінальній статті модуля ми розберемо процес проєктування API — покроковий алгоритм, code style, документування через OpenAPI/Swagger.

Copyright © 2026