Обеспечение безопасности 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 и подписи - возможны ошибки.
- Уязвимость при утечке секретного ключа - все токены становятся недействительными.
Расширенные примеры работы с токенами
Пример 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