Безпека 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://) допустимий тільки для локальної розробки (localhost). Будь-який продакшн API повинен працювати виключно через HTTPS. Сучасні браузери навіть маркують HTTP-сайти як «небезпечні».2. UUID замість послідовних ID
Проблема послідовних ID
Якщо ваш API використовує послідовні числові ідентифікатори (/v1/orders/42, /v1/orders/43), зловмисник може:
- Перебрати ID — запитати
/v1/orders/1,/v1/orders/2, ...,/v1/orders/100000і зібрати всі дані - Оцінити масштаб — якщо останнє замовлення має ID 50 000, зловмисник знає, що у вас ~50 000 замовлень
- Вгадати ресурс — якщо створив замовлення #42, то замовлення #41 належить іншому користувачу
UUID як рішення
UUID (Universally Unique Identifier) — 128-бітний ідентифікатор, що генерується випадково:
/v1/orders/550e8400-e29b-41d4-a716-446655440000
UUID неможливо перебрати (простір 2¹²⁸ ≈ 3.4 × 10³⁸ варіантів) і він не розкриває інформацію про кількість записів або порядок створення.
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);
});
3. JWT: безпека на практиці
Ми вже розглядали JWT як механізм stateless-авторизації у статті 05. Тепер зосередимось на безпекових аспектах.
Що зберігати в JWT
JWT-токен — це не сховище даних. Він підписаний, але не зашифрований — будь-хто може декодувати його через Base64. Тому:
| ✅ Можна зберігати | ❌ Не можна зберігати |
|---|---|
user_id (ідентифікатор) | Пароль |
roles (ролі) | Номер кредитної картки |
exp (час закінчення) | Медичні дані |
iat (час створення) | Персональні дані (GDPR) |
Час життя токена
Чим коротший час життя токена — тим безпечніше, але і менш зручно для користувача:
| Тип токена | Час життя | Призначення |
|---|---|---|
| Access token | 15-60 хвилин | Для запитів до API |
| Refresh token | 7-30 днів | Для отримання нового access token |
Чому два токени? Access token — короткоживучий, бо він передається у кожному запиті (більший ризик перехоплення). Refresh token — довгоживучий, використовується рідко (тільки для оновлення), і його можна відкликати на сервері.
Що робити при компрометації
Якщо access token викрадено, зловмисник може використовувати його до закінчення терміну дії. Зменшити ризик:
- Короткий час життя — через 15 хвилин токен просто перестане працювати
- Перевірка IP / fingerprint — токен прив'язаний до клієнта
- 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 | Часто змінюється, кешувати небезпечно |
| Помилка 4xx | no-store | Помилку кешувати не можна |
| Зображення логотипу | public, max-age=86400 | Майже ніколи не змінюється |
ETag + Cache-Control — комбінований підхід
Найефективніша стратегія кешування — комбінування Cache-Control з ETag:
- Сервер повертає
Cache-Control: max-age=300іETag: "v1" - Протягом 5 хвилин клієнт використовує локальний кеш (навіть не звертається до сервера)
- Після 5 хвилин клієнт робить умовний запит з
If-None-Match: "v1" - Якщо дані не змінились →
304 Not Modified(сервер не передає тіло) - Якщо змінились →
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. Простіше для клієнта, але ускладнює кешування (різні мови = різний кеш).
{
"id": 42,
"name": {
"uk": "Лунго",
"en": "Lungo",
"de": "Lungo"
},
"description": {
"uk": "Подовжений еспресо з м'яким смаком",
"en": "Extended espresso with a mild taste"
}
}
Клієнт сам обирає мову. Кеш один для всіх, але відповідь більша.
Vary: Accept-Language.Часові зони
Окреме джерело проблем — часові зони. Правила:
- Зберігайте усі дати в UTC на сервері
- Передавайте дати у форматі ISO 8601 з вказанням зони:
"2024-02-26T14:30:00Z"(Z = UTC) - Локалізацію часових зон залиште клієнту — мобільний додаток знає часову зону користувача
Ніколи не зберігайте дати у «локальному» часі на сервері. «14:00 у Києві» і «14:00 у Лондоні» — це різні моменти часу. Якщо сервер зберігає лише «14:00» без зони — ніхто не знає, що це означає.
6. Додаткові заходи безпеки
CORS (Cross-Origin Resource Sharing)
Якщо ваш API використовується з браузера (SPA-додатки), потрібно налаштувати CORS — механізм, що контролює, з яких доменів дозволені запити:
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: Базовий
Додайте правильні заголовки кешування:
GET /v1/recipes— публічний, кеш 1 година, з ETagGET /v1/profiles/{id}— приватний, без кешу, з ETagGET /v1/orders/{id}— без кешу взагалі- Помилки —
Cache-Control: no-store
Перепишіть API, замінивши числові ID на UUID:
POST /v1/products— генерація UUID на серверіGET /v1/products/{id}— прийом UUID- Переконайтесь, що некоректний UUID повертає
400, а неіснуючий —404
Рівень 2: Проєктування
Налаштуйте комплексну безпеку API:
- CORS — тільки для вашого домену
- Rate limiting — різні ліміти для GET/POST/auth
- Заголовки безпеки (X-Frame-Options, HSTS)
- JWT з коротким часом життя (15 хвилин)
- UUID для всіх зовнішніх ідентифікаторів
8. Резюме
TLS — обов'язковий
UUID замість integer
Cache-Control + ETag
i18n = UTC + Accept-Language
Далі: у фінальній статті модуля ми розберемо процес проєктування API — покроковий алгоритм, code style, документування через OpenAPI/Swagger.
Пагінація та організація списків
Limit/offset, курсорна пагінація, мутабельні та іммутабельні списки, зворотна пагінація, фільтри та сортування у списках.
Процес проєктування API та документування
Покроковий алгоритм проєктування API: від аналізу предметної області до документації. OpenAPI/Swagger, версіонування, code style та чеклист якості.