Обеспечение безопасности PHP-приложений через токены

Раздел: Веб-разработка -> Безопасность

Токены используются в веб-приложениях для защиты от подделки межсайтовых запросов (CSRF), аутентификации, одноразовых ссылок и API. Правильная реализация предотвращает утечки данных и несанкционированный доступ. В статье рассматриваются методы работы с токенами в PHP с примерами кода.

Основные подходы к реализации токенов в PHP

Как защитить формы от CSRF-атак с помощью сессионного токена?

Надёжный способ - хранить уникальный токен в сессии PHP и вставлять его в форму скрытым полем. Токен генерируется функцией random_bytes и преобразуется в шестнадцатеричную строку. При отправке POST-запроса сравнивается значение из формы и сессии.


<?php
session_start();

// Генерация CSRF токена
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="POST" action="/submit.php">
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    <button type="submit">Отправить</button>
</form>

<?php
// Проверка токена
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
        die('Invalid CSRF token');
    }
    // Обработка формы
}
?>
  

Php пароль mysql (пароль для mysql в php)

Функция hash_equals предотвращает атаки по времени. Сессионный токен автоматически живёт пока активна сессия.

Проблемы:

  • При XSS-уязвимости токен может быть украден из скрытого поля.
  • Если пользователь открывает несколько вкладок, токен в сессии перезаписывается - требуется генерация нового токена для каждой формы или использование массива токенов.
  • Токен не имеет срока жизни, если сессия бесконечна - рекомендуется добавлять метку времени.

Как создать одноразовый токен для сброса пароля?

Одноразовый токен генерируется, сохраняется в базе данных вместе с идентификатором пользователя и сроком действия. После использования или истечения времени токен удаляется.


<?php
// Генерация токена
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));

// Сохранение в БД (PDO)
$stmt = $pdo->prepare('INSERT INTO password_resets (user_id, token, expires_at) VALUES (?, ?, ?)');
$stmt->execute([$userId, $token, $expires]);

// Отправка ссылки
$link = "https://example.com/reset.php?token=" . urlencode($token);
?>
  

Api auth php (аутентификация api на php)

При проверке токен извлекается из БД, сравнивается с хешем (если хранить хеш токена) и проверяется срок действия.

Типичные ошибки:

  • Хранение токена в открытом виде в БД - уязвимость при утечке базы. Рекомендуется хранить хеш (sha256) и применять hash_equals.
  • Использование предсказуемых генераторов (uniqid, md5) - токен легко подобрать.
  • Отсутствие защиты от многократного использования - после первого применения токен должен быть немедленно удалён.

Как реализовать аутентификацию по JWT в PHP?

JSON Web Token (JWT) - способ передачи утверждений между сторонами. В PHP удобно использовать библиотеку firebase/php-jwt. Токен содержит заголовок, полезные данные и подпись.


<?php
require_once 'vendor/autoload.php';

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$key = 'ваш-секретный-ключ-256-бит';
$payload = [
    'user_id' => 123,
    'iat' => time(),
    'exp' => time() + 3600
];

$jwt = JWT::encode($payload, $key, 'HS256');
echo $jwt;

// Проверка
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
print_r($decoded);
?>
  

Protect php code (защита php кода)

Результат: строка JWT и после декодирования объект с данными. Подпись гарантирует целостность, но не шифрует данные.

  • JWT не может быть отозван до истечения срока - требуется чёрный список или короткий срок жизни.
  • Секретный ключ должен храниться вне кода (например, в .env) и иметь достаточную длину (не менее 256 бит для HS256).
  • При передаче через GET-параметр токен может быть сохранён в истории браузера. Передача только через заголовок Authorization.

Как подписать и проверить токен с помощью HMAC?

Альтернатива JWT - самодельный подписанный токен. Например, строка вида base64url(данные).base64url(подпись).


<?php
function generateToken(string $data, string $secret): string
{
    $payload = base64url_encode($data);
    $signature = hash_hmac('sha256', $payload, $secret, true);
    return $payload . '.' . base64url_encode($signature);
}

function validateToken(string $token, string $secret): ?string
{
    $parts = explode('.', $token);
    if (count($parts) !== 2) return null;
    $payload = $parts[0];
    $signature = base64url_decode($parts[1]);
    $expected = hash_hmac('sha256', $payload, $secret, true);
    if (!hash_equals($expected, $signature)) return null;
    return base64url_decode($payload);
}

function base64url_encode(string $data): string
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function base64url_decode(string $data): string
{
    return base64_decode(strtr($data, '-_', '+/'));
}

$secret = 'секрет';
$data = json_encode(['user' => 42, 'role' => 'admin']);
$token = generateToken($data, $secret);
echo $token . PHP_EOL;
$decoded = validateToken($token, $secret);
echo $decoded;
?>
  

Результат: подписанный токен и восстановленные данные.

Проблемы:

  • Отсутствие стандартной структуры - сложность интеграции с другими системами.
  • Ручная обработка base64url и подписи - возможны ошибки.
  • Уязвимость при утечке секретного ключа - все токены становятся недействительными.
- Https load php (загрузка через https в php)
- Domain block php (блокировка домена в php)
- Php action login (вход (login) через php)

Расширенные примеры работы с токенами

Пример 1. Класс для управления CSRF токенами с ротацией и временем жизни

Пример

<?php
class CsrfManager {
    private array $tokens = [];
    private int $maxTokens = 10;
    private int $lifetime = 3600; // 1 час

    public function __construct() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        $this->tokens = $_SESSION['csrf_tokens'] ?? [];
        $this->cleanExpired();
    }

    public function generate(): string {
        $token = bin2hex(random_bytes(32));
        $this->tokens[$token] = time();
        if (count($this->tokens) > $this->maxTokens) {
            array_shift($this->tokens);
        }
        $_SESSION['csrf_tokens'] = $this->tokens;
        return $token;
    }

    public function validate(string $token): bool {
        if (!isset($this->tokens[$token])) return false;
        $age = time() - $this->tokens[$token];
        if ($age > $this->lifetime) {
            unset($this->tokens[$token]);
            $_SESSION['csrf_tokens'] = $this->tokens;
            return false;
        }
        unset($this->tokens[$token]); // одноразовое использование
        $_SESSION['csrf_tokens'] = $this->tokens;
        return true;
    }

    private function cleanExpired(): void {
        foreach ($this->tokens as $token => $time) {
            if (time() - $time > $this->lifetime) {
                unset($this->tokens[$token]);
            }
        }
    }
}
?>

Этот класс решает проблему множественных вкладок и устаревших токенов. Каждая форма получает уникальный токен, который действителен в течение часа и может быть использован только один раз.

Пример использования в контроллере:
$csrf = new CsrfManager();
$token = $csrf->generate();
// передать $token в шаблон
// при обработке POST: $csrf->validate($_POST['csrf_token'])

Пример 2. JWT с обновлением токенов (refresh token) в PHP

Пример

<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class JwtAuth {
    private string $accessKey;
    private string $refreshKey;
    private int $accessExp = 900;  // 15 минут
    private int $refreshExp = 2592000; // 30 дней

    public function __construct() {
        $this->accessKey = $_ENV['JWT_ACCESS_SECRET'];
        $this->refreshKey = $_ENV['JWT_REFRESH_SECRET'];
    }

    public function createAccessToken(array $user): string {
        $payload = [
            'sub' => $user['id'],
            'role' => $user['role'],
            'iat' => time(),
            'exp' => time() + $this->accessExp
        ];
        return JWT::encode($payload, $this->accessKey, 'HS256');
    }

    public function createRefreshToken(int $userId): string {
        $payload = [
            'sub' => $userId,
            'type' => 'refresh',
            'iat' => time(),
            'exp' => time() + $this->refreshExp
        ];
        return JWT::encode($payload, $this->refreshKey, 'HS256');
    }

    public function refreshAccessToken(string $refreshToken): ?string {
        try {
            $decoded = JWT::decode($refreshToken, new Key($this->refreshKey, 'HS256'));
            if ($decoded->type !== 'refresh') return null;
            // проверить, не отозван ли refresh токен (например, в Redis)
            return $this->createAccessToken(['id' => $decoded->sub, 'role' => 'user']);
        } catch (\Exception $e) {
            return null;
        }
    }

    public function validateAccessToken(string $token): ?object {
        try {
            return JWT::decode($token, new Key($this->accessKey, 'HS256'));
        } catch (\Exception $e) {
            return null;
        }
    }
}
?>

Результат: два токена - короткоживущий access и долгоживущий refresh. Refresh токен хранится в безопасном HTTP-only cookie или на стороне клиента, access передаётся в заголовке.

$auth = new JwtAuth();
$access = $auth->createAccessToken(['id' => 1, 'role' => 'admin']);
$refresh = $auth->createRefreshToken(1);
echo "Access: $access\nRefresh: $refresh";
// Через 15 минут access истёк:
$newAccess = $auth->refreshAccessToken($refresh);
echo "New access: $newAccess";

Пример 3. Токен на основе HMAC с привязкой к IP и User-Agent

Пример

<?php
function generateBoundToken(array $data, string $secret): string {
    $data['ip'] = $_SERVER['REMOTE_ADDR'];
    $data['ua'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
    $json = json_encode($data);
    $payload = base64url_encode($json);
    $signature = hash_hmac('sha256', $payload, $secret, true);
    return $payload . '.' . base64url_encode($signature);
}

function validateBoundToken(string $token, string $secret): ?array {
    $parts = explode('.', $token);
    if (count($parts) !== 2) return null;
    $payload = $parts[0];
    $signature = base64url_decode($parts[1]);
    $expected = hash_hmac('sha256', $payload, $secret, true);
    if (!hash_equals($expected, $signature)) return null;
    $data = json_decode(base64url_decode($payload), true);
    // проверка привязки
    if ($data['ip'] !== $_SERVER['REMOTE_ADDR']) return null;
    if ($data['ua'] !== ($_SERVER['HTTP_USER_AGENT'] ?? '')) return null;
    return $data;
}
?>

Такой токен нельзя использовать с другого IP или браузера, что снижает риск перехвата.

$secret = 'ваш-секрет';
$token = generateBoundToken(['user_id' => 123], $secret);
echo $token;
$result = validateBoundToken($token, $secret);
print_r($result);
// Если IP изменится - вернёт null

Работа с токенами в PHP - comments

En
Index php php id token (php)