Аутентификация по API-ключу для веб-приложения
API-ключи — простейший механизм аутентификации для машинного взаимодействия (server-to-server). Нет OAuth-flow, нет refresh токенов, нет сессий. Клиент передаёт ключ в заголовке или параметре запроса, сервер его проверяет. Подходит для публичных API, интеграций с партнёрами, CLI-инструментов.
Генерация и хранение ключей
Ключ должен быть достаточно случайным — минимум 32 байта:
// Генерация ключа
$key = 'sk_' . bin2hex(random_bytes(32)); // sk_ + 64 hex = 67 символов
// Пример: sk_a3f9b12e8c4d7e1f0a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5
// Никогда не храним ключ в открытом виде — только hash
$hash = hash('sha256', $key);
DB::table('api_keys')->insert([
'user_id' => $userId,
'name' => $request->name,
'key_prefix' => substr($key, 0, 8), // для отображения пользователю
'key_hash' => $hash,
'scopes' => json_encode(['read:articles', 'write:articles']),
'last_used_at' => null,
'expires_at' => now()->addYear(),
]);
// Ключ показываем пользователю ОДИН РАЗ — при создании
return response()->json(['key' => $key], 201);
Храним только хэш — при утечке базы ключи бесполезны.
Проверка ключа
// Middleware ApiKeyAuth
public function handle(Request $request, Closure $next): Response
{
$key = $request->bearerToken() // Authorization: Bearer sk_...
?? $request->header('X-Api-Key') // X-Api-Key: sk_...
?? $request->query('api_key'); // ?api_key=sk_... (избегать в URL)
if (!$key) {
return response()->json(['error' => 'API key required'], 401);
}
$hash = hash('sha256', $key);
$apiKey = ApiKey::where('key_hash', $hash)
->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
->first();
if (!$apiKey) {
return response()->json(['error' => 'Invalid or expired API key'], 401);
}
// Обновляем last_used_at (асинхронно, чтобы не замедлять запрос)
dispatch(fn() => $apiKey->update(['last_used_at' => now()]))->afterResponse();
$request->setUserResolver(fn() => $apiKey->user);
$request->attributes->set('api_key', $apiKey);
return $next($request);
}
Scopes (разрешения)
Ключ должен иметь минимально необходимые права:
// Проверка scope в контроллере или Middleware
public function store(Request $request): JsonResponse
{
$apiKey = $request->attributes->get('api_key');
if (!in_array('write:articles', $apiKey->scopes ?? [])) {
return response()->json(['error' => 'Insufficient scope'], 403);
}
// ...
}
Список scopes определяется при создании ключа пользователем (или выдаётся фиксированный набор).
Безопасность
Передача ключа:
- Только через HTTPS
- Предпочтительно в заголовке
Authorization: BearerилиX-Api-Key - Не в URL (попадает в логи, историю браузера, referer)
Ротация ключей:
// Invalidate старый ключ при создании нового
ApiKey::where('user_id', $userId)->where('name', $name)->delete();
Аудит использования:
ApiKeyUsageLog::create([
'api_key_id' => $apiKey->id,
'ip' => $request->ip(),
'endpoint' => $request->path(),
'method' => $request->method(),
'status' => null, // заполняется в Terminate middleware
]);
Rate limiting по ключу:
RateLimiter::for('api-key', function (Request $request) {
$apiKey = $request->attributes->get('api_key');
return Limit::perMinute($apiKey->rate_limit ?? 60)->by($apiKey->id);
});
UX в личном кабинете
Список ключей с отображением key_prefix (первые 8 символов):
sk_a3f9b1... "Production integration" Последнее использование: 2 часа назад [Удалить]
sk_c2d4e5... "Staging webhook" Ни разу не использован [Удалить]
Добавить кнопку «Показать ключ» не получится — он не хранится. Только пересоздание.
Сроки
Таблица api_keys, middleware проверки, UI создания/удаления ключей, rate limiting по ключу: 1–2 дня.







