Безопасность через токены в 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 пароль mysql (пароль для mysql в php)
- Domain block php (блокировка домена в php)
- Auth php (аутентификация в php)

Расширенные примеры использования токенов в 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.

Токены в PHP - comments

En
Tokens php (php)