Безопасность через токены в PHP: методы и примеры кода
Основные принципы работы с токенами в PHP
Токены в PHP - это случайные или подписанные строки, используемые для подтверждения подлинности запросов, идентификации сессий, защиты от CSRF-атак и других сценариев безопасности.
Наиболее эффективное решение: криптостойкая генерация и сессионное хранение CSRF-токена
Как создать и проверить CSRF-токен с минимальными рисками?
// Генерация токена с использованием random_bytes
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Вывод токена в форму
?>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<button type="submit">Отправить</button>
</form>
<?php
// Проверка токена при отправке
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('Недействительный токен.');
}
// Обработка формы
}
Access php (доступ к файлам в php)
Пояснение: функция random_bytes(32) генерирует 32 байта случайных данных, которые затем переводятся в шестнадцатеричную строку длиной 64 символа. Токен хранится в сессии, что исключает его утечку через куки или URL.
Типичная ошибка: использование mt_rand() или uniqid() - эти функции не являются криптостойкими. Исправить заменой на random_bytes() или openssl_random_pseudo_bytes().
Как реализовать подписанный токен с помощью HMAC?
Подписанный токен полезен, когда нужно проверить целостность данных без хранения в сессии (например, для ссылок подтверждения email).
$secret = 'my_very_secret_key_32_symbols';
$data = ['user_id' => 42, 'exp' => time() + 3600];
$payload = base64_encode(json_encode($data));
$signature = hash_hmac('sha256', $payload, $secret);
$token = $payload . '.' . $signature;
// Проверка
$parts = explode('.', $token);
if (hash_hmac('sha256', $parts[0], $secret) === $parts[1]) {
$decoded = json_decode(base64_decode($parts[0]), true);
if ($decoded['exp'] > time()) {
echo 'Токен действителен для пользователя ' . $decoded['user_id'];
}
}
Php filter (фильтрация данных в php)
Проблемы: утечка секретного ключа делает все токены подделываемыми. Используйте переменные окружения для хранения ключа. Также следует избегать слишком длинных данных в payload.
Как применить JWT для аутентификации API?
JWT (JSON Web Token) - стандартизированный формат подписанных токенов, часто используется в REST API.
composer require firebase/php-jwt
use \Firebase\JWT\JWT;
use \Firebase\JWT\Key;
$key = 'example_key';
$payload = [
'iss' => 'http://example.org',
'aud' => 'http://example.com',
'iat' => time(),
'exp' => time() + 3600,
'user_id' => 1
];
$jwt = JWT::encode($payload, $key, 'HS256');
echo $jwt;
// Декодирование
try {
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
print_r($decoded);
} catch (Exception $e) {
echo 'Ошибка: ' . $e->getMessage();
}
Php пароль (работа с паролями в php)
Типичная ошибка: использование слабого алгоритма (например, 'none') или хранение ключа внутри кода. Всегда указывайте алгоритм явно и не берите секрет из открытых источников.
Как реализовать одноразовые токены для сброса пароля?
Токены, которые можно использовать только один раз и с ограниченным сроком жизни, часто хранятся в базе данных.
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + 3600);
// Сохранить в БД: INSERT INTO password_reset (user_id, token, expires) VALUES (?, ?, ?)
// В email отправить ссылку: http://example.com/reset?token=$token
// Проверка при переходе
$stmt = $db->prepare('SELECT user_id, expires FROM password_reset WHERE token = ?');
$stmt->execute([$_GET['token']]);
$row = $stmt->fetch();
if ($row && strtotime($row['expires']) > time()) {
// Позволить сменить пароль и удалить токен
$db->exec('DELETE FROM password_reset WHERE token = ?', [$_GET['token']]);
}
Tokens php (токены в php)
Проблема race condition: если пользователь нажимает ссылку дважды, второй запрос может не найти токен. Решением является транзакция с блокировкой или уникальным индексом.
Как создать токен для API с длительным сроком жизни (Bearer token)?
Пример генерации и хранения статического токена (например, для доступа к публичному API).
$api_token = bin2hex(random_bytes(32));
// Сохранить в БД вместе с идентификатором пользователя
// Клиент передаёт токен в заголовке: Authorization: Bearer $token
// Проверка
$headers = getallheaders();
$token = str_replace('Bearer ', '', $headers['Authorization'] ?? '');
$stmt = $db->prepare('SELECT user_id FROM api_tokens WHERE token = ?');
$stmt->execute([$token]);
$user = $stmt->fetch();
if (!$user) {
http_response_code(401);
exit;
}
Не рекомендуется хранить токены в открытом виде в базе. Лучше хранить хеш токена и проверять через password_verify или hash_equals.
Расширенные примеры использования токенов в PHP
Пример 1: Многоразовый CSRF-токен с временем жизни (автоматическая ротация)
session_start();
function generateCsrfToken(): string {
if (!isset($_SESSION['csrf_tokens'])) {
$_SESSION['csrf_tokens'] = [];
}
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_tokens'][$token] = time();
// Удалить токены старше 30 минут
foreach ($_SESSION['csrf_tokens'] as $t => $time) {
if ($time < time() - 1800) {
unset($_SESSION['csrf_tokens'][$t]);
}
}
return $token;
}
function verifyCsrfToken(string $token): bool {
if (isset($_SESSION['csrf_tokens'][$token])) {
if ($_SESSION['csrf_tokens'][$token] > time() - 1800) {
unset($_SESSION['csrf_tokens'][$token]); // одноразовое использование
return true;
}
unset($_SESSION['csrf_tokens'][$token]);
}
return false;
}
// Использование
$csrf_token = generateCsrfToken();
// В форме: <input type="hidden" name="csrf_token" value="<?= $csrf_token ?>">
// Проверка: if (!verifyCsrfToken($_POST['csrf_token'])) { die('CSRF fail'); }
// Результат: токен может быть использован только один раз в течение 30 минут. Старые токены удаляются автоматически.
Пример 2: JWT с асимметричным шифрованием (RS256)
// Генерация ключей: openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
// openssl rsa -pubout -in private.pem -out public.pem
use \Firebase\JWT\JWT;
use \Firebase\JWT\Key;
$privateKey = file_get_contents('/path/to/private.pem');
$publicKey = file_get_contents('/path/to/public.pem');
$payload = [
'sub' => 123,
'name' => 'John Doe',
'iat' => time(),
'exp' => time() + 60
];
$jwt = JWT::encode($payload, $privateKey, 'RS256');
echo 'Созданный JWT: ' . $jwt . PHP_EOL;
// Проверка
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
print_r($decoded);
// Вывод: объект с полями sub, name, iat, exp. Идентичность подтверждена публичным ключом.
Пример 3: Токены с защитой от воспроизведения через nonce
// Сервер генерирует nonce и возвращает клиенту вместе с CSRF-токеном
$nonce = bin2hex(random_bytes(16));
$csrf = bin2hex(random_bytes(32));
// Сохраняем пару (nonce, csrf) в сессии
$_SESSION['nonce_tokens'][$nonce] = $csrf;
// Отдаём форму с двумя скрытыми полями
// <input type="hidden" name="nonce" value="$nonce">
// <input type="hidden" name="csrf_token" value="$csrf">
// Проверка
if (isset($_POST['nonce'], $_POST['csrf_token'])) {
$expected = $_SESSION['nonce_tokens'][$_POST['nonce']] ?? null;
if ($expected && hash_equals($expected, $_POST['csrf_token'])) {
unset($_SESSION['nonce_tokens'][$_POST['nonce']]);
// Токен действителен
}
}
// Связка nonce + токен предотвращает возможность подстановки старого захваченного токена, так как nonce уникален для каждого запроса.
Пример 4: Хранение хешированных API-токенов с солью
// Генерация
$raw_token = bin2hex(random_bytes(32));
$hash = password_hash($raw_token, PASSWORD_BCRYPT);
// Сохраняем $hash в базе, отдаём клиенту $raw_token
// Проверка при запросе
$input_token = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; // Bearer xxx
$input_token = str_replace('Bearer ', '', $input_token);
$stmt = $db->query('SELECT token_hash FROM api_tokens WHERE user_id = ?');
$hash_from_db = $stmt->fetchColumn();
if (password_verify($input_token, $hash_from_db)) {
echo 'Токен верен';
} else {
http_response_code(401);
}
// Даже если база данных скомпрометирована, злоумышленник не сможет восстановить исходный токен. Bcrypt включает соль автоматически.
Пример 5: Использование signed double-submit cookie для CSRF (без сессии)
$secret = 'my_secret_key_for_csrf';
$cookie_name = 'csrf_token';
$cookie_value = bin2hex(random_bytes(32));
$signature = hash_hmac('sha256', $cookie_value, $secret);
setcookie($cookie_name, $cookie_value, 0, '/', '', true, true); // httpOnly, secure
// В форме: <input type="hidden" name="csrf_token" value="$cookie_value">
// и скрытое поле с сигнатурой: <input type="hidden" name="csrf_signature" value="$signature">
// Проверка на сервере
if (isset($_COOKIE[$cookie_name], $_POST['csrf_token'], $_POST['csrf_signature'])) {
$expected_sig = hash_hmac('sha256', $_POST['csrf_token'], $secret);
if ($_COOKIE[$cookie_name] === $_POST['csrf_token'] && hash_equals($expected_sig, $_POST['csrf_signature'])) {
echo 'CSRF проверка пройдена';
}
}
// Метод не требует хранения состояния на сервере, но защищает только от атак, где злоумышленник не может прочитать cookie.