← Cloudflare Turnstile / turnstile / tutorials
Выявление мошенничества с помощью Ephemeral IDs
Ephemeral IDs позволяют выявлять схемы мошенничества, которые обходят традиционные методы обнаружения по IP. В этом руководстве показано, как записывать Ephemeral ID в журнал, находить подозрительные закономерности и блокировать нарушителей.
Злоумышленники нередко создают сотни фальшивых аккаунтов ради злоупотребления акциями, перебирают пулы прокси, чтобы обойти ограничение частоты запросов по IP, и работают через настоящие браузеры, чтобы уйти от простых способов обнаружения ботов.
Обычное определение по IP-адресу здесь не работает: каждый запрос выглядит так, будто пришёл с другого адреса. Ephemeral ID решают эту задачу, потому что определяют само клиентское устройство, даже когда IP-адреса меняются.
Перед началом работы
- Для Ephemeral ID требуется Enterprise Bot Management на Дополнение Enterprise Turnstile, или отдельная версия Enterprise Turnstile. Чтобы включить эту функцию, обратитесь к своей команде по работе с аккаунтом.
- Вам потребуется базовое понимание интеграции Turnstile. См. Начало работы с Turnstile, где это описано подробнее.
Настройка логирования
Создайте таблицу для хранения событий с Ephemeral ID.
CREATE TABLE turnstile_events (
id BIGSERIAL PRIMARY KEY,
ephemeral_id VARCHAR(64) NOT NULL,
event_type VARCHAR(50) NOT NULL, -- 'signup', 'login', 'checkout'
ip_address VARCHAR(45),
user_id VARCHAR(128), -- NULL for signups, populated after
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE blocked_ephemeral_ids (
ephemeral_id VARCHAR(64) PRIMARY KEY,
reason VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW()
);Извлеките Ephemeral ID и запишите их в журнал
При вызове Siteverify Ephemeral ID возвращается в metadata поле. Записывайте его при каждом защищённом действии.
async function verifyAndLogTurnstile(
token: string,
ip: string,
secretKey: string,
eventType: string,
db: Database,
): Promise<{ success: boolean; ephemeralId?: string; isBlocked: boolean }> {
// Call Siteverify API
const response = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: secretKey,
response: token,
remoteip: ip,
}),
},
);
const result = await response.json();
if (!result.success) {
return { success: false, isBlocked: false };
}
const ephemeralId = result.metadata?.ephemeral_id;
if (ephemeralId) {
// Log the event
await db.query(
`INSERT INTO turnstile_events (ephemeral_id, event_type, ip_address)
VALUES ($1, $2, $3)`,
[ephemeralId, eventType, ip],
);
// Check if already blocked
const blocked = await db.query(
`SELECT 1 FROM blocked_ephemeral_ids WHERE ephemeral_id = $1`,
[ephemeralId],
);
if (blocked.rows.length > 0) {
return { success: true, ephemeralId, isBlocked: true };
}
}
return { success: true, ephemeralId, isBlocked: false };
}Используйте Ephemeral ID в сценарии регистрации
export async function handleSignup(request: Request, env: Env) {
const formData = await request.formData();
const email = formData.get("email") as string;
const turnstileToken = formData.get("cf-turnstile-response") as string;
const ip = request.headers.get("CF-Connecting-IP") || "";
// Verify Turnstile and log the Ephemeral ID
const verification = await verifyAndLogTurnstile(
turnstileToken,
ip,
env.TURNSTILE_SECRET_KEY,
"signup",
env.DB,
);
if (!verification.success) {
return new Response("Verification failed", { status: 400 });
}
// Block if this device is flagged
if (verification.isBlocked) {
// Return a generic message - don't reveal detection
return new Response("Please verify your email to continue", {
status: 202,
});
}
// Proceed with normal signup
const userId = await createUser(email, formData.get("password"));
// Update the log with the new user ID
if (verification.ephemeralId) {
await env.DB.query(
`UPDATE turnstile_events
SET user_id = $1
WHERE ephemeral_id = $2 AND event_type = 'signup' AND user_id IS NULL
ORDER BY created_at DESC LIMIT 1`,
[userId, verification.ephemeralId],
);
}
return new Response("Account created", { status: 201 });
}Выявление мошеннических схем
Периодически выполняйте следующий запрос (например, каждые пять минут), чтобы находить подозрительные Ephemeral ID:
-- Find devices creating multiple accounts in the last hour
SELECT
ephemeral_id,
COUNT(*) as signup_count,
COUNT(DISTINCT ip_address) as unique_ips
FROM turnstile_events
WHERE
event_type = 'signup'
AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY ephemeral_id
HAVING COUNT(*) > 3; -- More than 3 signups = suspiciousНайденные подозрительные идентификаторы блокируйте:
INSERT INTO blocked_ephemeral_ids (ephemeral_id, reason)
SELECT
ephemeral_id,
'Multiple signups: ' || COUNT(*) || ' in 1 hour'
FROM turnstile_events
WHERE
event_type = 'signup'
AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY ephemeral_id
HAVING COUNT(*) > 3
ON CONFLICT (ephemeral_id) DO NOTHING;Разберитесь и примите меры
Когда вы блокируете аккаунты за злоупотребления, находите другие аккаунты с того же устройства:
-- Find all accounts created from the same device as a banned user
SELECT DISTINCT te2.user_id, te2.created_at
FROM turnstile_events te1
JOIN turnstile_events te2 ON te1.ephemeral_id = te2.ephemeral_id
WHERE te1.user_id = 'BANNED_USER_ID'
AND te2.user_id IS NOT NULL
AND te2.user_id != 'BANNED_USER_ID';Массово пометить аккаунты для разбора:
-- Flag all accounts from a suspicious device
UPDATE users
SET status = 'under_review'
WHERE id IN (
SELECT DISTINCT user_id
FROM turnstile_events
WHERE ephemeral_id = 'x:SUSPICIOUS_ID_HERE'
AND user_id IS NOT NULL
);Рекомендации
- Записывать в журнал сразу: фиксируйте Ephemeral ID сразу при вызове Siteverify.
- Скрытый отказ: блокируя мошенничество, возвращайте общие сообщения об ошибках. Никогда не сообщайте, что вы распознали устройство.
- Настройте пороговые значения: начните с осторожных значений в запросе (например, три регистрации в час) и скорректируйте их по своему трафику.
- Сочетание сигналов: используйте Ephemeral ID вместе с репутацией IP-адресов и поведенческой аналитикой.