Аутентификация запросов к веб-сервисам на PHP
Основные подходы к аутентификации API в PHP
Наиболее эффективное решение для современных API - аутентификация на основе JWT (JSON Web Token). JWT - это компактный, самодостаточный токен, содержащий утверждения (claims), подписанный секретным ключом или парой ключей. Сервер не хранит сессии, что упрощает масштабирование и распределённые системы.
Реализация JWT аутентификации
Используется библиотека firebase/php-jwt. Установка через Composer:
composer require firebase/php-jwtAccess php (доступ к файлам в php)
Генерация токена
Создаётся токен после успешной аутентификации (логин + пароль):
<?
use Firebase\JWT\JWT;
require 'vendor/autoload.php';
$key = 'your-secret-key-here';
$payload = [
'iss' => 'example.com', // издатель
'iat' => time(), // время выпуска
'exp' => time() + 3600, // срок действия (1 час)
'sub' => $userId, // идентификатор пользователя
'roles' => ['user', 'admin'] // дополнительные утверждения
];
$jwt = JWT::encode($payload, $key, 'HS256');
echo $jwt;
?>Php filter (фильтрация данных в php)
Проверка токена в middleware
Клиент передаёт токен в заголовке Authorization: Bearer
<?
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$key = 'your-secret-key-here';
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
if (!preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
http_response_code(401);
exit('Токен не предоставлен');
}
try {
$decoded = JWT::decode($matches[1], new Key($key, 'HS256'));
$userId = $decoded->sub;
// Далее работаем с аутентифицированным пользователем
} catch (\Exception $e) {
http_response_code(401);
exit('Недействительный токен: ' . $e->getMessage());
}
?>Php пароль (работа с паролями в php)
Типичные ошибки и их решение:
- Истечение срока токена (exp) – клиент должен обновлять токен через refresh-механизм или заново логиниться.
- Неверный алгоритм подписи – указывать точное значение 'HS256' или 'RS256' при декодировании.
- Утечка секретного ключа – хранить ключ в переменных окружения, не коммитить в репозиторий.
- Межсайтовая подделка запроса (CSRF) – при использовании JWT в куках, токен в заголовке Bearer не подвержен CSRF.
Как реализовать простую аутентификацию без сторонних библиотек?
API Ключи (Bearer Token)
Самый простой вариант – назначать каждому клиенту статический токен (API-ключ) и проверять его при каждом запросе. Токен может быть хеширован в базе данных.
// Генерация ключа
$token = bin2hex(random_bytes(32));
echo $token;Tokens php (токены в php)
// Проверка
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
$stmt = $pdo->prepare('SELECT id FROM users WHERE api_key = ?');
$stmt->execute([hash('sha256', $apiKey)]);
$user = $stmt->fetch();
if (!$user) {
http_response_code(403);
exit('Неверный ключ');
}
?>Https load php (загрузка через https в php)
Проблемы: статические ключи сложно отзывать, нет автоматического истечения, требуется защищённое хранение на стороне клиента.
Как защитить API при помощи стандартной HTTP Basic-аутентификации?
HTTP Basic Auth
Клиент передаёт логин и пароль в заголовке Authorization: Basic base64(login:password). Способ прост, но пароль передаётся в открытом виде (только через HTTPS).
<?
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="My API"');
http_response_code(401);
exit('Требуется аутентификация');
}
$login = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
// Проверка в БД
if (!verifyUser($login, $password)) {
http_response_code(403);
exit('Неверные учётные данные');
}
?>права php (управление правами доступа в php)
Недостатки: каждый запрос требует проверки логина/пароля, сложность с logout, уязвимость к перехвату при неиспользовании HTTPS.
Как внедрить OAuth 2.0 для сторонних приложений?
OAuth 2.0 (например, Google/Facebook)
Позволяет делегировать аутентификацию внешним провайдерам. PHP-приложение выступает в роли ресурсного сервера. Требуется библиотека (например, league/oauth2-server или интеграция с провайдером).
// Пример с Google Client
$client = new Google\Client();
$client->setClientId(GOOGLE_CLIENT_ID);
$payload = $client->verifyIdToken($idToken);
if ($payload) {
$userId = $payload['sub'];
// Создаём собственную сессию или JWT
}
?>Php пароль mysql (пароль для mysql в php)
Сложности: настройка Redirect URI, хранение refresh-токенов, защита от CSRF в flow авторизации.
Как аутентифицировать запросы с подписью (HMAC)?
HMAC подпись
Клиент создаёт подпись запроса с помощью секретного ключа и хеш-функции. Сервер проверяет подпись, используя тот же ключ. Подходит для сервер-серверных интеграций.
// Создание подписи на клиенте
$secret = 'shared-secret';
$data = 'POST:/api/data:' . json_encode($body);
$signature = hash_hmac('sha256', $data, $secret);
// Заголовок X-Signature: $signatureDomain block php (блокировка домена в php)
// Проверка на сервере
$expected = hash_hmac('sha256', 'POST:/api/data:' . file_get_contents('php://input'), $secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'])) {
http_response_code(401);
exit('Подпись не совпадает');
}
?>
Проблемы: защита от replay-атак (требуется включение timestamp в данные), сложность отзыва ключа.
Дополнительные примеры кода
Работа с refresh-токенами в JWT
Для продления сессии без повторного ввода пароля используют пару access/refresh. Access-токен живёт коротко (15 мин), refresh – долго (30 дней).
// Генерация пары
$accessPayload = [
'sub' => $userId,
'exp' => time() + 900, // 15 минут
'type' => 'access'
];
$accessToken = JWT::encode($accessPayload, $key, 'HS256');
$refreshPayload = [
'sub' => $userId,
'exp' => time() + 2592000, // 30 дней
'type' => 'refresh',
'jti' => bin2hex(random_bytes(16)) // уникальный ID для отзыва
];
$refreshToken = JWT::encode($refreshPayload, $key, 'HS256');
// Сохраняем jti в БД для последующей проверки
Эндпоинт обновления:
// /api/refresh
$refreshToken = $_POST['refresh_token'] ?? '';
try {
$decoded = JWT::decode($refreshToken, new Key($key, 'HS256'));
if ($decoded->type !== 'refresh') throw new Exception('Не refresh-токен');
// Проверяем jti в БД (не отозван)
$userId = $decoded->sub;
// Выдаём новый access-токен
$newAccess = JWT::encode([
'sub' => $userId,
'exp' => time() + 900,
'type' => 'access'
], $key, 'HS256');
echo json_encode(['access_token' => $newAccess]);
} catch (\Exception $e) {
http_response_code(401);
echo json_encode(['error' => 'Refresh token invalid']);
}
?>
Аутентификация с помощью HMAC и защиты от replay
Добавим timestamp и nonce, чтобы запрос не мог быть повторён злоумышленником.
// Клиент
$secret = 'my-secret';
$timestamp = time();
$nonce = bin2hex(random_bytes(8));
$body = '{"action":"read"}';
$dataToSign = $timestamp . ':' . $nonce . ':' . $body;
$signature = hash_hmac('sha256', $dataToSign, $secret);
// Заголовки: X-Timestamp, X-Nonce, X-Signature
// Сервер
$receivedTimestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? 0;
$receivedNonce = $_SERVER['HTTP_X_NONCE'] ?? '';
$receivedSignature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
// Проверка временной метки (разница не более 5 минут)
if (abs(time() - $receivedTimestamp) > 300) {
http_response_code(401);
exit('Запрос устарел');
}
// Проверка nonce (хранить в redis на 5 минут)
if (nonceIsUsed($receivedNonce)) {
http_response_code(401);
exit('Nonce уже использован');
}
// Проверка подписи
$expected = hash_hmac('sha256', $receivedTimestamp . ':' . $receivedNonce . ':' . $body, $secret);
if (!hash_equals($expected, $receivedSignature)) {
http_response_code(401);
exit('Подпись недействительна');
}
?>
Использование атрибутов PHP для middleware (PSR-7 / Slim Framework)
Пример с Slim Framework 4, где аутентификация реализована в middleware с проверкой JWT.
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\App;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$app = new App();
$jwtMiddleware = function (Request $request, $handler) use ($key) {
$authHeader = $request->getHeaderLine('Authorization');
if (!preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write('Unauthorized');
return $response->withStatus(401);
}
try {
$decoded = JWT::decode($matches[1], new Key($key, 'HS256'));
$request = $request->withAttribute('user_id', $decoded->sub);
return $handler->handle($request);
} catch (\Exception $e) {
$response = new \Slim\Psr7\Response();
$response->getBody()->write('Invalid token');
return $response->withStatus(401);
}
};
$app->get('/api/protected', function (Request $request, Response $response) {
$userId = $request->getAttribute('user_id');
$response->getBody()->write("Hello user $userId");
return $response;
})->add($jwtMiddleware);
$app->run();
Пример с использованием API-ключей и лимитов запросов
// Генерация ключа при регистрации
$rawKey = bin2hex(random_bytes(32));
$hashedKey = password_hash($rawKey, PASSWORD_BCRYPT);
// Сохраняем $hashedKey в БД, возвращаем $rawKey клиенту
// Проверка
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
$stmt = $pdo->prepare('SELECT id, rate_limit FROM users WHERE api_key_hash = ?');
$stmt->execute([hash('sha256', $apiKey)]);
$user = $stmt->fetch();
if (!$user) {
http_response_code(403);
exit('Invalid API key');
}
// Проверка лимита (например, Redis)
$currentCount = $redis->incr("rate:{$user['id']}");
if ($currentCount > $user['rate_limit']) {
http_response_code(429);
exit('Rate limit exceeded');
}
?>
Результат выполнения (для последнего примера) – при превышении лимита сервер вернёт 429 Too Many Requests.