Zamek czasowy – jak zabezpieczyć zasoby strony WWW przed botami
Co to jest zamek czasowy?
Zamek czasowy to technika zabezpieczenia zasobów (np. plików, danych API) polegająca na tym, że dostęp do nich jest możliwy tylko w określonym, krótkim czasie – np. kilka minut od wygenerowania linku lub tokena. Po upływie tego czasu dostęp staje się niemożliwy.
Mechanizm ten bywa używany np. do:
- zabezpieczania linków do pobierania plików,
- ochrony prywatnych zasobów API,
- ograniczania masowego scrapowania treści strony.
Jak to działa?
Mechanizm działania zamka czasowego opiera się zazwyczaj na:
- Tokenie: unikalnym ciągu znaków generowanym po stronie serwera.
- Znacznikiem czasu (timestamp): oznaczającym moment wygenerowania tokena.
- Podpisie HMAC lub hashu: weryfikującym, że token nie został sfałszowany.
Przykładowy token:
GET /api/data?token=abc123&expires=1716124800
Serwer przyjmuje żądanie tylko wtedy, gdy:
expires
< aktualny czas,token
jest zgodny z podpisem wygenerowanym po stronie serwera.
Przykład w PHP i AJAX
Krok 1: Serwer – generowanie tokena
<?php
function generate_token($secret, $valid_for_seconds = 300) {
$expires = time() + $valid_for_seconds;
$data = "expires=$expires";
$signature = hash_hmac('sha256', $data, $secret);
return [
'expires' => $expires,
'token' => $signature
];
}
// Przykład użycia
$secret = 'tajny_klucz';
$token_data = generate_token($secret);
?>
<script>
const token = "<?= $token_data['token'] ?>";
const expires = <?= $token_data['expires'] ?>;
</script>
Krok 2: Klient – pobieranie danych AJAX z tokenem
fetch(`/api/data.php?token=${token}&expires=${expires}`)
.then(res => res.json())
.then(data => {
console.log("Dane:", data);
});
Krok 3: Serwer – weryfikacja tokena
<?php
// api/data.php
$secret = 'tajny_klucz';
$expires = $_GET['expires'] ?? 0;
$token = $_GET['token'] ?? '';
if (time() > $expires) {
http_response_code(403);
echo json_encode(['error' => 'Token expired']);
exit;
}
$data = "expires=$expires";
$expected_token = hash_hmac('sha256', $data, $secret);
if (!hash_equals($expected_token, $token)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid token']);
exit;
}
// Wszystko OK – zwróć dane
header('Content-Type: application/json');
echo json_encode(['message' => 'Tajne dane API']);
Czy to zabezpiecza przed botami?
Częściowo. Oto, co daje, a czego nie daje zamek czasowy:
Zalety:
- Utrudnia dostęp do zasobów dla zewnętrznych botów (np. scrapperów), które nie wykonują całego procesu ładowania strony i pozyskania tokena.
- Chroni tymczasowe zasoby (np. pliki do pobrania, dane API) przed nadmiernym i nieautoryzowanym dostępem.
Ograniczenia:
- Jeśli bot potrafi uruchamiać JavaScript (np. przez Puppeteer lub Selenium), może przechwycić token jak normalna przeglądarka.
- Token można podejrzeć w DevTools – jeśli nie ma dodatkowej autoryzacji (np. sesji/logowania), może być przekazywany innym.
- Tokeny mogą być masowo generowane z serwera, jeśli endpoint nie ma rate-limitingu.
Jak poprawić bezpieczeństwo?
Aby zwiększyć skuteczność zamka czasowego:
- Powiąż token z sesją użytkownika – dodaj
user_id
lub IP do HMAC. - Ustaw limit użycia tokena – np. jednokrotne użycie (wymaga bazy danych lub cache).
- Dodaj rate limiting – np. przez Redis.
- Wymagaj uwierzytelnienia – nawet jeśli to tylko sesja lub JWT.
- Utrzymuj ważność tokenów krótko – 1–5 minut to rozsądna wartość.
Wersja zamka z odczekiwaniem użytkownika
Zamek czasowy z odczekiwaniem użytkownika to wersja mechanizmu, która wymusza czekanie określonego czasu przed dostępem do danych lub zasobów – na przykład 5 sekund po wejściu na stronę, zanim możliwe będzie pobranie danych z API. Używa się go m.in. w celu:
- zmniejszenia ataku botów działających „na szybko”,
- zwiększenia zaangażowania użytkownika (np. przed wyświetleniem reklamy),
- ochrony przed nadużyciami (np. masowym klikaniem w przyciski).
Jak to zrealizować: przykład krok po kroku
Cel: użytkownik musi poczekać 5 sekund, zanim będzie mógł wykonać żądanie AJAX do API.
Koncepcja
- Strona ładowana przez użytkownika zawiera informację o czasie startu.
- Po stronie klienta (JavaScript) liczony jest czas, np.
5 sekund
. - Po tym czasie generowany jest token (lub tylko aktywowany przycisk).
- Dopiero po tym czasie można pobrać dane z API.
- Serwer może dodatkowo sprawdzić, czy żądanie nie zostało wysłane zbyt wcześnie (opcjonalnie).
Przykład – Zamek czasowy z oczekiwaniem 5 sekund (PHP + JS)
index.php
(frontend i generowanie danych startowych):
<?php
session_start();
$secret = 'tajny_klucz';
// Zapisz czas wejścia użytkownika do sesji
$_SESSION['start_time'] = time();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Zamek czasowy</title>
</head>
<body>
<h1>Dane będą dostępne za 5 sekund...</h1>
<button id="getData" disabled>Pobierz dane</button>
<pre id="output"></pre>
<script>
const waitSeconds = 5;
const button = document.getElementById('getData');
const output = document.getElementById('output');
// Odczekaj wymagany czas
setTimeout(() => {
button.disabled = false;
button.textContent = "Pobierz dane teraz";
}, waitSeconds * 1000);
button.addEventListener('click', () => {
fetch('api.php')
.then(res => res.json())
.then(data => output.textContent = JSON.stringify(data, null, 2))
.catch(err => output.textContent = 'Błąd: ' + err);
});
</script>
</body>
</html>
api.php
(backend – dostęp dopiero po czasie)
<?php
session_start();
// Minimalny czas oczekiwania (w sekundach)
$minWait = 5;
if (!isset($_SESSION['start_time'])) {
http_response_code(403);
echo json_encode(['error' => 'Brak sesji']);
exit;
}
$elapsed = time() - $_SESSION['start_time'];
if ($elapsed < $minWait) {
http_response_code(429);
echo json_encode([
'error' => 'Zbyt szybki dostęp',
'waited_seconds' => $elapsed,
'required_seconds' => $minWait
]);
exit;
}
// Wszystko OK – zwróć dane
header('Content-Type: application/json');
echo json_encode([
'message' => 'Dostęp do tajnych danych uzyskany!',
'timestamp' => time()
]);
Czy to zabezpiecza przed botami?
Tak, częściowo. Ten mechanizm nie zatrzyma zaawansowanych botów (np. z headless przeglądarkami), ale:
- Zatrzyma proste curl/wget/scrapery, które nie obsługują JavaScript i nie „czekają” 5 sekund.
- Wymusza aktywność użytkownika (kliknięcie) – co utrudnia automatyczne zmasowane pobieranie.
- W połączeniu z Rate Limiting + CAPTCHA tworzy solidną zaporę.
Możliwe ulepszenia
- Token czasowy z podpisem – użytkownik dostaje token dopiero po czasie, który musi potem wysłać z AJAX-em.
- Weryfikacja IP lub User-Agent – sprawdzenie, czy żądanie nie pochodzi od podejrzanego klienta.
- Dodanie CAPTCHA przed aktywacją przycisku.
- Pojedyncze użycie – zapis do Redis/MySQL, że dany token już był użyty.
Klasa Time_Lock
dla Koseven Framework
Poniżej znajdziesz propozycję klasy Time_Lock
dla frameworka Koseven, która umożliwia:
- Założenie zamka czasowego – np. przy pierwszym wejściu użytkownika.
- Sprawdzenie, czy zamek nadal działa – czy wymagany czas minął.
- Zwolnienie zamka – jeśli chcesz ręcznie go wyczyścić.
Ta klasa może być używana do implementacji drugiego podejścia (z oczekiwaniem), np. przed umożliwieniem AJAX-em dostępu do danych. Zamki są przechowywane w sesji użytkownika ($_SESSION
) i identyfikowane po kluczu ($lock_id
), np. data_access
// application/classes/Time/Lock.php
class Time_Lock {
/**
* Założenie zamka czasowego.
*
* @param string $lock_id Unikalna nazwa zamka
* @param int $wait_seconds Ile sekund trzeba czekać
*/
public static function set(string $lock_id, int $wait_seconds): void
{
Session::instance()->set("lock_time_$lock_id", time());
Session::instance()->set("lock_duration_$lock_id", $wait_seconds);
}
/**
* Sprawdza, czy zamek jest aktywny (czy trzeba jeszcze czekać).
*
* @param string $lock_id
* @return bool true = zamek aktywny (dostęp ZABLOKOWANY)
*/
public static function is_locked(string $lock_id): bool
{
$start = Session::instance()->get("lock_time_$lock_id");
$duration = Session::instance()->get("lock_duration_$lock_id");
if (!$start || !$duration) {
return false; // Brak zamka
}
return (time() - $start) < $duration;
}
/**
* Ile jeszcze sekund trzeba czekać.
*
* @param string $lock_id
* @return int Pozostały czas w sekundach (lub 0)
*/
public static function remaining(string $lock_id): int
{
$start = Session::instance()->get("lock_time_$lock_id");
$duration = Session::instance()->get("lock_duration_$lock_id");
if (!$start || !$duration) {
return 0;
}
$remaining = $duration - (time() - $start);
return max(0, $remaining);
}
/**
* Zwolnienie zamka (np. po udanym oczekiwaniu).
*
* @param string $lock_id
*/
public static function release(string $lock_id): void
{
Session::instance()->delete("lock_time_$lock_id");
Session::instance()->delete("lock_duration_$lock_id");
}
}
Przykład użycia
1. Założenie zamka przy pierwszym wejściu
// w kontrolerze np. przy wejściu na stronę
Time_Lock::set('data_api', 5); // 5 sekund czekania
2. Sprawdzenie zamka przy żądaniu AJAX
// application/classes/Controller/Api/Data.php
class Controller_Api_Data extends Controller {
public function action_index()
{
if (Time_Lock::is_locked('data_api')) {
$this->response->status(429);
$this->response->body(json_encode([
'error' => 'Zbyt szybki dostęp',
'remaining_seconds' => Time_Lock::remaining('data_api'),
]));
return;
}
// (opcjonalnie) zwolnij zamek, jeśli to jednorazowy dostęp
Time_Lock::release('data_api');
$this->response->headers('Content-Type', 'application/json');
$this->response->body(json_encode([
'message' => 'Dostęp uzyskany po czasie!',
]));
}
}
Przykład w JavaScript
Przycisk staje się aktywny po 5 sekundach (frontend wymusza oczekiwanie):
<button id="btn" disabled>Pobierz dane</button>
<pre id="output"></pre>
<script>
const button = document.getElementById('btn');
const output = document.getElementById('output');
setTimeout(() => {
button.disabled = false;
button.textContent = "Kliknij, aby pobrać dane";
}, 5000);
button.addEventListener('click', () => {
fetch('/api/data')
.then(res => res.json())
.then(data => output.textContent = JSON.stringify(data, null, 2));
});
</script>
Uwaga
Ten zamek nie chroni zasobu przed:
- Użytkownikiem, który ominie frontend i wyśle żądanie AJAX zewnętrznie.
- Zaawansowanymi botami.
Aby poprawić bezpieczeństwo:
- Powiąż zamek z
user_id
lub adresem IP. - Dodaj tokeny podpisane (np. HMAC).
- Ogranicz użycia (rate limit, TTL, licznik prób).
Podsumowanie
Zamek czasowy to przydatny sposób na tymczasowe ograniczenie dostępu do zasobów strony, szczególnie API lub linków do pobierania. Sam w sobie nie zastępuje pełnych zabezpieczeń, ale w połączeniu z innymi technikami (autoryzacja, rate limiting) skutecznie podnosi poziom bezpieczeństwa Twojej aplikacji.