Реализация входа в веб-API на PHP
Создание API входа на PHP требует понимания различных подходов к аутентификации. Выбор зависит от архитектуры приложения: монолитная или микросервисная, наличие клиентских приложений (SPA, мобильные), требования к безопасности. Рассмотрим несколько вариантов, от простого сессионного механизма до токенов JWT и протокола OAuth2.
Основное решение: JWT аутентификация (stateless)
JWT (JSON Web Token) - наиболее эффективное решение для современных API, работающих с клиентами на разных платформах. Сервер не хранит состояния сессии, вся информация о пользователе закодирована в токене, который подписывается секретным ключом.
1. Установка библиотеки
Используем firebase/php-jwt через Composer:
composer require firebase/php-jwtApi login php (api входа на php)
2. Генерация токена при входе
use \Firebase\JWT\JWT;
use \Firebase\JWT\Key;
$secretKey = 'ваш_секретный_ключ'; // минимум 32 символа
function generateToken($userId, $username) {
$payload = [
'iss' => 'your-api',
'iat' => time(),
'exp' => time() + 3600, // 1 час
'sub' => $userId,
'name' => $username
];
return JWT::encode($payload, $secretKey, 'HS256');
}
Эндпоинт /login принимает логин и пароль, проверяет их (например, через bcrypt) и возвращает токен.
3. Проверка токена (middleware)
function authenticateToken($headers) {
if (!isset($headers['Authorization'])) {
http_response_code(401);
echo json_encode(['error' => 'Токен не предоставлен']);
exit;
}
$token = str_replace('Bearer ', '', $headers['Authorization']);
try {
$decoded = JWT::decode($token, new Key($secretKey, 'HS256'));
return $decoded; // объект с sub, name и т.д.
} catch (\Exception $e) {
http_response_code(401);
echo json_encode(['error' => 'Неверный токен: ' . $e->getMessage()]);
exit;
}
}
Типичные ошибки:
- Токен истёк - клиент должен обновить токен через refresh token или повторный вход.
- Секретный ключ слабый - используйте случайную строку длиной не менее 256 бит.
- Алгоритм подписи не указан явно - в firebase/php-jwt версии 6+ обязательно передавать объект Key.
Как реализовать сессионную аутентификацию для API?
Традиционный подход с использованием нативных PHP-сессий (PHPSESSID). Подходит для монолитных приложений, где клиент и сервер на одном домене, или при использовании cookie.
session_start();
// Вход
if ($password_verify) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
echo json_encode(['message' => 'ok']);
}
// Проверка
function requireAuth() {
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Не авторизован']);
exit;
}
}
Проблемы:
- Не подходит для мобильных приложений или SPA, работающих на другом домене (CORS, CSRF).
- Сервер хранит состояние - сложно масштабировать горизонтально (требуется общее хранилище сессий, например Redis).
- Уязвимость к CSRF-атакам - необходимы токены для форм.
Как использовать API-ключи для аутентификации?
Простой вариант для серверных приложений или микросервисов. Клиент передаёт ключ в заголовке или параметре запроса.
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? $_GET['api_key'] ?? '';
$validKeys = ['key1_secret', 'key2_secret']; // хранить в базе
if (!in_array($apiKey, $validKeys)) {
http_response_code(403);
echo json_encode(['error' => 'Неверный ключ']);
exit;
}
Недостатки:
- Ключ передаётся открыто - используйте HTTPS.
- Нет привязки к конкретному пользователю - только к приложению.
- Сложно отозвать отдельный ключ без изменения списка.
Как реализовать OAuth2 с входом через Google?
Стандартный протокол для делегирования аутентификации. Клиент получает токен доступа от стороннего провайдера.
// Используем библиотеку league/oauth2-google
$provider = new \League\OAuth2\Client\Provider\Google([
'clientId' => 'ВАШ_CLIENT_ID',
'clientSecret' => 'ВАШ_SECRET',
'redirectUri' => 'https://ваш-сайт/callback',
]);
// Перенаправление на Google
if (!isset($_GET['code'])) {
$authUrl = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
header('Location: ' . $authUrl);
exit;
}
// Получение токена
$token = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
echo json_encode(['access_token' => $token->getToken()]);
Сложности:
- Необходимость регистрации приложения у провайдера.
- Хранение refresh token для долгосрочного доступа.
- Обработка ошибок (отказ пользователя, истекший код).
Как сделать HMAC-подпись запросов?
Безопасный способ для серверных интеграций: запрос подписывается секретным ключом без его передачи.
// Сервер: проверка подписи
$secret = 'shared_secret';
$signature = hash_hmac('sha256', $requestBody, $secret);
$clientSignature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!hash_equals($signature, $clientSignature)) {
http_response_code(401);
exit;
}
Внимание: Необходимо защищать от replay-атак добавлением временной метки и nonce.
Расширенные примеры с полным кодом и результатами.
Пример 1: Полный класс JWT-аутентификации
class Auth {
private static $secretKey = 'A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6';
private static $algorithm = 'HS256';
private static $expire = 3600;
public static function login($username, $password) {
// проверка в БД (упрощено)
$user = ['id' => 1, 'password_hash' => password_hash('pass123', PASSWORD_BCRYPT)];
if (!password_verify($password, $user['password_hash'])) {
return null;
}
$payload = [
'iss' => 'example.com',
'iat' => time(),
'exp' => time() + self::$expire,
'sub' => $user['id'],
'username' => $username
];
return JWT::encode($payload, self::$secretKey, self::$algorithm);
}
public static function validate($token) {
try {
return JWT::decode($token, new Key(self::$secretKey, self::$algorithm));
} catch (\Exception $e) {
return null;
}
}
}
Результат успешного входа:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJleGFtcGxlLmNvbSIsImlhdCI6MTY5ODc2NTQzMiwiZXhwIjoxNjk4NzY5MDMyLCJzdWIiOjEsInVzZXJuYW1lIjoidXNlcjEifQ.s9I4aF6iG0k7Rc9Xz2A3bPqZ7uQ1vW5yJ0dL8kBn3YQ
Пример 2: Сессионный API с Redis
// Настройка Redis как хранилище сессий
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://127.0.0.1:6379');
session_start();
// Авторизация
$_SESSION['user'] = ['id' => 1, 'role' => 'admin'];
// Возвращаем ID сессии
$sessionId = session_id();
echo json_encode(['session_id' => $sessionId]);
Ответ клиента:
{"session_id": "abc123xyz..."}
Клиент передаёт этот ID в cookie PHPSESSID при каждом запросе.
Пример 3: OAuth2 - получение профиля Google
// После получения access_token от Google
$resourceOwner = $provider->getResourceOwner($token);
$user = $resourceOwner->toArray();
echo json_encode([
'email' => $user['email'],
'name' => $user['name'],
'avatar' => $user['picture']
]);
Результат:
{"email": "user@gmail.com", "name": "Иван Иванов", "avatar": "https://..."}
Пример 4: HMAC-подпись с временной меткой
// Клиент формирует подпись
$data = ['action' => 'getUser', 'timestamp' => time()];
$secret = 'shared_secret';
$signature = hash_hmac('sha256', json_encode($data), $secret);
// Отправляет POST запрос с заголовками X-Signature, X-Timestamp
// Сервер проверяет
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'];
if (abs(time() - $timestamp) > 300) { // 5 минут
die('Устаревший запрос');
}
$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256', $payload, 'shared_secret');
if (hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'])) {
echo json_encode(['status' => 'ok']);
} else {
http_response_code(403);
}
Примечание: hash_equals защищает от атак по времени.