198 lines
5.7 KiB
PHP
198 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries;
|
|
|
|
/**
|
|
* RateLimitIdentifier — генератор идентификаторов для rate limiting
|
|
*
|
|
* Использует комбинацию:
|
|
* - Cookie token (если есть) — персональный идентификатор браузера
|
|
* - IP адрес — базовая идентификация
|
|
* - User Agent hash — дополнительная уникальность
|
|
*/
|
|
class RateLimitIdentifier
|
|
{
|
|
private const COOKIE_NAME = 'rl_token';
|
|
private const COOKIE_TTL = 365 * 24 * 3600; // 1 год
|
|
|
|
/**
|
|
* Получает уникальный идентификатор для rate limiting
|
|
*
|
|
* @param string $action Действие (login, register, reset)
|
|
* @return string Хеш-идентификатор
|
|
*/
|
|
public function getIdentifier(string $action): string
|
|
{
|
|
$parts = [];
|
|
|
|
// Cookie token — если есть, добавляем
|
|
$cookieToken = $_COOKIE[self::COOKIE_NAME] ?? null;
|
|
if ($cookieToken && $this->isValidToken($cookieToken)) {
|
|
$parts['c'] = $cookieToken;
|
|
}
|
|
|
|
// IP адрес — всегда
|
|
$parts['i'] = $this->getClientIp();
|
|
|
|
// User Agent hash — всегда (для дополнительной уникальности)
|
|
$parts['ua'] = $this->getUserAgentHash();
|
|
|
|
// Если куки нет — добавляем признак "без куки" для отладки
|
|
if (empty($cookieToken)) {
|
|
$parts['nc'] = '1';
|
|
}
|
|
|
|
// Комбинация всех частей → хеш
|
|
return md5('rl:' . $action . ':' . implode('|', $parts));
|
|
}
|
|
|
|
/**
|
|
* Генерирует и устанавливает токен, если его нет
|
|
*
|
|
* @return string|null Токен или null если уже есть
|
|
*/
|
|
public function ensureToken(): ?string
|
|
{
|
|
if (empty($_COOKIE[self::COOKIE_NAME])) {
|
|
$token = $this->generateToken();
|
|
|
|
// Устанавливаем куку на год
|
|
setcookie(
|
|
self::COOKIE_NAME,
|
|
$token,
|
|
[
|
|
'expires' => time() + self::COOKIE_TTL,
|
|
'path' => '/',
|
|
'secure' => true,
|
|
'samesite' => 'Lax',
|
|
'httponly' => true,
|
|
]
|
|
);
|
|
|
|
return $token;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Проверяет, установлен ли токен
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasToken(): bool
|
|
{
|
|
return !empty($_COOKIE[self::COOKIE_NAME]);
|
|
}
|
|
|
|
/**
|
|
* Получает JS код для установки токена при первом визите
|
|
*
|
|
* @return string JavaScript код
|
|
*/
|
|
public function getJsScript(): string
|
|
{
|
|
return <<<JS
|
|
(function() {
|
|
const cookieName = 'rl_token';
|
|
|
|
// Проверяем, есть ли кука
|
|
const hasCookie = document.cookie.split('; ').find(function(row) {
|
|
return row.indexOf(cookieName + '=') === 0;
|
|
});
|
|
|
|
if (!hasCookie) {
|
|
// Генерируем UUID v4 на клиенте
|
|
function generateUUID() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
const token = generateUUID();
|
|
const date = new Date();
|
|
date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000));
|
|
|
|
const cookieString = cookieName + '=' + token +
|
|
'; expires=' + date.toUTCString() +
|
|
'; path=/' +
|
|
'; SameSite=Lax' +
|
|
(location.protocol === 'https:' ? '; Secure' : '') +
|
|
'; HttpOnly';
|
|
|
|
document.cookie = cookieString;
|
|
}
|
|
})();
|
|
JS;
|
|
}
|
|
|
|
/**
|
|
* Генерирует уникальный токен
|
|
*
|
|
* @return string 32 символа (16 байт в hex)
|
|
*/
|
|
private function generateToken(): string
|
|
{
|
|
return bin2hex(random_bytes(16));
|
|
}
|
|
|
|
/**
|
|
* Проверяет валидность токена
|
|
*
|
|
* @param string $token
|
|
* @return bool
|
|
*/
|
|
private function isValidToken(string $token): bool
|
|
{
|
|
// Токен должен быть 32 hex символа (16 байт)
|
|
return preg_match('/^[a-f0-9]{32}$/', $token) === 1;
|
|
}
|
|
|
|
/**
|
|
* Получает IP адрес клиента
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getClientIp(): string
|
|
{
|
|
$ipKeys = [
|
|
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
|
'HTTP_X_REAL_IP', // Nginx proxy
|
|
'HTTP_X_FORWARDED_FOR', // Load balancer
|
|
'REMOTE_ADDR',
|
|
];
|
|
|
|
foreach ($ipKeys as $key) {
|
|
if (!empty($_SERVER[$key])) {
|
|
$ips = explode(',', $_SERVER[$key]);
|
|
return trim($ips[0]);
|
|
}
|
|
}
|
|
|
|
return '0.0.0.0';
|
|
}
|
|
|
|
/**
|
|
* Получает хеш User Agent
|
|
*
|
|
* Используем хеш вместо полного UA для:
|
|
* - Короче значение
|
|
* - Не храним чувствительные данные
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getUserAgentHash(): string
|
|
{
|
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|
|
|
// Обрезаем слишком длинные UA
|
|
if (strlen($ua) > 500) {
|
|
$ua = substr($ua, 0, 500);
|
|
}
|
|
|
|
return md5($ua);
|
|
}
|
|
}
|