[ ENGINEERING_GUIDE ][ RECAPTCHA ][ BEZPIECZEŃSTWO ][ FORMULARZE ][ NEXTJS ]

reCAPTCHA v3 — niewidoczna ochrona formularzy przed spamem (kompletny przewodnik 2026)

10 czerwca 202612 min czytania
Autor: DevStudio.itStudio Web & AI

Token po stronie klienta, weryfikacja w API route, próg score 0.5, lazyOnload w layout.tsx, RODO i warstwy honeypot/rate limit — wzorzec z produkcyjnej strony Next.js.

READ_TIME: 12 MIN_COMPLEXITY: MED_
STAMP: VERIFIED_BY_DS_

TL;DR

reCAPTCHA v3 nie pokazuje checkboxa „Nie jestem robotem” — zwraca score 0.0–1.0 i krótkotrwały token, który musisz zweryfikować po stronie serwera (siteverify), zanim zapiszesz lead w bazie lub wyślesz mail. Na produkcyjnej stronie firmowej Next.js 15 skrypt ładuje się lazyOnload w layout.tsx (żeby nie psuć LCP), a formularz kontaktowy czeka na grecaptcha.ready z timeoutem 12 s, bo inaczej token bywa pusty i API zwraca 400. Próg 0.5 to rozsądny start; obok tego warto mieć rate limit po IP (w tym projekcie 5 zgłoszeń / minutę) i rozważyć honeypot oraz ten sam wzorzec dla chatbota. Poniżej: pełny przepływ, RODO, fałszywe odrzucenia i testy.

Dla kogo to jest

  • Firm usługowych z formularzem kontaktowym lub chatbotem generującym leady
  • Developerów na Next.js / React, którzy chcą ochrony bez irytującego captcha v2
  • Osób odpowiedzialnych za RODO / DSGVO — disclosure przy formularzu i transfer danych do Google
  • Zespołów widzących spam w skrzynce mimo „jakiegoś captcha” w motywie WordPress — i szukających wzorca server-side

Fraza (SEO)

recaptcha v3 formularz kontaktowy, ochrona formularza przed spamem, recaptcha nextjs api route, recaptcha score próg, weryfikacja recaptcha serwer, lazyOnload recaptcha layout, rodo recaptcha google

Dlaczego v3, a nie checkbox v2

Google reCAPTCHA ewoluowało od widocznych łamigłówek do niewidocznej oceny ryzyka. W wersji v3 użytkownik zwykle nie klika niczego — skrypt analizuje sygnały (historia interakcji z domeną, wzorzec ruchu, fingerprint) i zwraca score. To lepsze UX na stronie B2B, gdzie kafr „wybierz wszystkie sygnalizatory” obniża konwersję.

Aspekt reCAPTCHA v2 (checkbox) reCAPTCHA v3
UX Widoczna łamigłówka Niewidoczne; badge w rogu
Wynik pass / fail score 0.0–1.0
Gdzie decyzja głównie u Google w UI Ty na serwerze (próg)
Wydajność Cięższy iframe Lżejszy, ale nadal zewnętrzny skrypt

v3 nie zastępuje walidacji pól, rate limitu ani sanity-checków — to jedna warstwa w obronie głębokiej (defence in depth).

Architektura na stronie Next.js 15 (wzorzec produkcyjny)

W projekcie software house (strona wielojęzyczna /pl, /en, /de) ochrona opiera się na trzech miejscach: globalny skrypt w layoucie, wykonanie execute() przy submit formularza, weryfikacja w /api/submissions.

Skrypt w layout.tsxlazyOnload

Tag reCAPTCHA nie ładuje się afterInteractive jak GA4 — celowo lazyOnload, żeby nie konkurować o pierwszy render z tagami analitycznymi i nie obciążać LCP. Klucz witryny (site key) jest publiczny i trafia do URL:

<Script
  src="https://www.google.com/recaptcha/api.js?render=6Lcwsx0sAAAAAO4sbP31qTdgjuoGLgMmp9HyxhYB"
  strategy="lazyOnload"
/>

Komentarz w kodzie mówi wprost: skrypt jest lazy — formularz musi poczekać na ready, inaczej token nie powstanie.

Badge i informacja prawna — dwa poziomy

Google wymaga widoczności badge reCAPTCHA. W layoucie CSS obcina badge do małej ikony w lewym dolnym rogu (overflow hidden), a pełne zdanie prawne stoi przy formularzu kontaktowym w page.tsx — z linkami do Polityki prywatności i Warunków Google (tłumaczenia w messages/pl.json, en.json, de.json):

{translations.contact.form.recaptchaBeforePrivacy}
<a href="https://policies.google.com/privacy">...</a>
{translations.contact.form.recaptchaBetween}
<a href="https://policies.google.com/terms">...</a>
{translations.contact.form.recaptchaAfter}

To ważne pod RODO: użytkownik wie, że dane mogą trafić do Google LLC w ramach antyspamowej analizy — nie wystarczy sam ukryty badge.

Po stronie klienta — token przy wysyłce formularza

W handleSubmit formularza kontaktowego nie wywołuj execute() na onClick bez oczekiwania — przy lazyOnload window.grecaptcha bywa undefined przez pierwsze sekundy.

Wzorzec: czekaj na ready, potem execute

await new Promise((resolve, reject) => {
  const t = setTimeout(() => reject(new Error('recaptcha_timeout')), 12000);
  const done = () => { clearTimeout(t); resolve(); };
  if (window.grecaptcha?.ready) {
    window.grecaptcha.ready(done);
  } else {
    const iv = setInterval(() => {
      if (window.grecaptcha?.ready) {
        clearInterval(iv);
        window.grecaptcha.ready(done);
      }
    }, 100);
    setTimeout(() => {
      clearInterval(iv);
      if (!window.grecaptcha?.ready) reject(new Error('recaptcha_no_script'));
    }, 12000);
  }
});

const recaptchaToken = await window.grecaptcha.execute(
  '6Lcwsx0sAAAAAO4sbP31qTdgjuoGLgMmp9HyxhYB',
  { action: 'submit_contact_form' }
);

Action (submit_contact_form) pojawia się w panelu reCAPTCHA i w logach — osobne akcje dla chatbota (submit_chatbot_form) ułatwiają analizę score per kanał.

Token wysyłasz w body POST razem z polami leada:

const response = await fetch('/api/submissions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ ...data, locale, recaptchaToken }),
});

Jeśli execute się nie uda, użytkownik dostaje czytelny komunikat („Poczekaj chwilę po załadowaniu strony…”) — nie wysyłasz pustego tokena na siłę.

Typy TypeScript dla grecaptcha trzymamy w src/types/gtag.d.ts obok gtag, żeby uniknąć błędów kompilacji.

Po stronie serwera — weryfikacja w API route

Secret key nigdy nie trafia do przeglądarki. W Vercel / .env ustawiasz RECAPTCHA_SECRET_KEY i w POST /api/submissions wołasz:

const recaptchaResponse = await fetch(
  'https://www.google.com/recaptcha/api/siteverify',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: `secret=${recaptchaSecret}&response=${recaptchaToken}`,
  }
);
const recaptchaData = await recaptchaResponse.json();

Kolejność decyzji w produkcji:

  1. Brak tokena → HTTP 400 (boty często omijają JS).
  2. success: false (np. timeout-or-duplicate, zły secret) → 400 + log error-codes.
  3. score < 0.5 → 400 (bot / podejrzany ruch).
  4. Dopiero potem walidacja pól i zapis do Prisma + maile transakcyjne.
if (recaptchaData.score !== undefined && recaptchaData.score < 0.5) {
  return NextResponse.json(
    { error: 'Weryfikacja nie powiodła się. Odśwież stronę i spróbuj ponownie.' },
    { status: 400 }
  );
}

Token jest jednorazowy i krótko żyje — nie cache’uj go między requestami. Przy błędzie sieci do Google blokujesz zgłoszenie (fail closed), bo otwarcie endpointu bez weryfikacji = zaproszenie dla spammerów.

Próg score — jak dobrać 0.5, 0.7 czy „miękki”

Google sugeruje traktować score jako sygnał, nie absolut. W praktyce:

Próg Efekt Kiedy
0.3 Mało fałszywych odrzuceń, więcej spamu Bardzo niski ruch, testy A/B
0.5 Balans (domyślny w wielu tutorialach) Strony firmowe, lead B2B
0.7 Agresywniejszy filtr Formularze z bonusem (e-book, kupon)

W opisywanym projekcie start to 0.5. Po tygodniu warto zajrzeć w Admin Console reCAPTCHA → Analytics i zobaczyć rozkład score dla action submit_contact_form. Jeśli 5% legitnych leadów ma score 0.45–0.49, rozważ 0.45 albo dodatkową warstwę (rate limit) zamiast twardego 0.7.

Miękki próg: score 0.3–0.5 → zapis do kolejki „do ręcznej weryfikacji” zamiast twardego odrzucenia — przydatne przy kampaniach Ads z ruchem z nowych urządzeń (VPN, Safari ITP).

Rate limit — pierwsza linia obrony (już w projekcie)

Zanim w ogóle dojdzie do Google, endpoint /api/submissions woła checkEmailRateLimit(ip) z src/lib/email-rate-limit.ts:

  • 5 żądań na IP na 60 sekund
  • przy przekroczeniu: HTTP 429 + nagłówek Retry-After

To chroni przed prostymi skryptami i floodem, który wyczerpałby limit maili transakcyjnych. Ten sam helper jest na innych endpointach mailowych (chatbot, umowy, testimoniale).

Rate limit nie zastępuje reCAPTCHA — bot z wielu IP nadal przejdzie. Razem dają sensowny koszt ataku.

Honeypot — kiedy dodać trzecią warstwę

W tym repozytorium nie ma jeszcze pola honeypot — i to OK przy v3 + rate limit na umiarkowanym ruchu. Honeypot warto dodać, gdy:

  • widzisz powtarzalny spam z poprawnym score (farmy botów),
  • masz prosty formularz HTML łatwy do scrapowania,
  • chcesz zero kosztu (ukryte pole website, company_url — CSS position:absolute; left:-9999px, tabIndex={-1}, autoComplete="off").

Serwer odrzuca request, gdy honeypot niepusty — bez komunikatu „jesteś botem” (żeby nie uczyć atakującego). Kolejność: rate limit → honeypot → reCAPTCHA → walidacja biznesowa.

Chatbot — ten sam wzorzec, osobny endpoint

Formularz kontaktowy korzysta z /api/submissions i pełnej weryfikacji reCAPTCHA. Endpoint chatbota /api/chatbot/submit w chwili pisania ma rate limit, ale nie weryfikuje tokena — typowa luka, gdy spammer odkryje JSON API.

Rekomendowany plan (zgodny z resztą architektury):

  1. Wyodrębnij verifyRecaptchaToken(token, minScore) do src/lib/recaptcha.ts.
  2. W komponencie chatbota — ten sam blok ready + execute z action submit_chatbot_form.
  3. W POST /api/chatbot/submit — identyczna ścieżka weryfikacji co w submissions.
  4. Disclosure reCAPTCHA przy finalnym kroku chatbota (skrócona wersja tekstu z formularza).

Dzięki temu jeden klucz witryny, jeden secret, dwie akcje w panelu Google — łatwiejsze debugowanie niż dwa osobne projekty reCAPTCHA.

RODO, cookies i polityka prywatności

reCAPTCHA v3 przetwarza dane użytkownika u Google (USA — mechanizmy transferu: SCC, DPF w zależności od aktualnego stanu prawnego). Minimalny pakiet compliance:

  • Informacja przy formularzu (linki Google Privacy + Terms) — już w UI.
  • W polityce prywatności strony: cel (antyspam), kategorie danych (IP, cookies Google, sygnały zachowania), podstawa prawna (uzasadniony interes art. 6 ust. 1 lit. f lub zgoda — skonsultuj z prawnikiem przy marketingowych cookies).
  • Jeśli baner cookies blokuje skrypty Google do zgody — reCAPTCHA może nie działać; wtedy albo kategoria „niezbędne” dla antyspam, albo ładowanie po zgodzie z jasnym komunikatem przy formularzu.
  • Retention: logi score w Twoim API trzymaj krótko; nie zapisuj tokena reCAPTCHA w bazie leadów na stałe.

Nie udawaj, że v3 jest „bezcookies” — badge i dokumentacja Google mówią inaczej.

Fałszywe odrzucenia (false positives) — co robić

Użytkownicy z VPN, Tor, świeżymi profilami przeglądarki, adblockiem blokującym google.com/recaptcha, albo korporacyjnym proxy czasem dostają niski score mimo legitnej intencji.

Objawy w logach:

  • recaptcha_no_script / recaptcha_timeout po stronie klienta,
  • score too low z wartością 0.1–0.4 przy sensownym opisie projektu,
  • skargi „formularz nie działa” tylko na Firefox z ETP.

Remediacja:

  1. Komunikat zachęcający do odświeżenia / maila bezpośredniego (kontakt@...) — już lepsze niż generic 400.
  2. Obniżenie progu tymczasowo + monitoring.
  3. Fallback: drugi kanał (telefon, Calendly) widoczny obok formularza.
  4. Nie przechodź na sam honeypot „bo captcha zła” — stracisz ochronę przy targetowanym spamie.

Testuj z produkcyjnej domeny — localhost w panelu reCAPTCHA musi być na liście domen testowych, inaczej success: false.

Testowanie — checklist 15 minut

Panel Google reCAPTCHA

  1. Typ kluczy: v3, domeny: produkcja + opcjonalnie localhost.
  2. Skopiuj site key (public) i secretRECAPTCHA_SECRET_KEY na Vercel.
  3. Po deployu: zakładka Overview — czy rosną requesty dla action submit_contact_form.

DevTools

  1. Network → submit formularza → payload JSON z recaptchaToken (długi string).
  2. Response 200 tylko gdy score OK.
  3. Celowo wyślij bez tokena (curl) → 400.

Symulacja niskiego score

Google nie daje przełącznika „bądź botem” w v3; do testów użyj test keys z dokumentacji Google albo tymczasowo obniż próg na stagingu.

Checklist przed kampanią Ads

  • Secret ustawiony na produkcji (nie tylko lokalnie)
  • lazyOnload + timeout 12 s na ready
  • Disclosure przy formularzu we wszystkich locale
  • Rate limit aktywny
  • Chatbot (jeśli publiczny) — ten sam verify co formularz
  • Logi serwera bez wycieku secret key

Wydajność vs. bezpieczeństwo

lazyOnload dla reCAPTCHA to świadomy kompromis jak przy tagach Google Ads: LCP ważniejszy niż natychmiastowa gotowość captcha. Formularze B2B wypełnia się zwykle po 30+ sekundach — wtedy skrypt jest już załadowany. Wyjątek: landing z formularzem „above the fold” i bardzo krótkim czasem decyzji — rozważ afterInteractive tylko dla reCAPTCHA albo preload na stronie kontaktowej.

Nie ładuj reCAPTCHA na każdej podstronie bloga, jeśli masz tylko jeden formularz na home — w tym projekcie skrypt jest globalny (prostsze), ale przy optymalizacji Performance można warunkowo <Script> tylko tam, gdzie jest <form>.

Typowe błędy wdrożenia (i jak ich uniknąć)

  1. Secret w .env.local, brak na Vercel — lokalnie działa, produkcja zwraca 500 „Konfiguracja reCAPTCHA nie jest kompletna”. Po każdym nowym środowisku sprawdź RECAPTCHA_SECRET_KEY w dashboardzie hostingu.
  2. Weryfikacja tylko gdy token istnieje — w dobrym wzorcu brak tokena = blokada, nie „przepuść, bo może adblock”. W /api/submissions pusty recaptchaToken kończy się 400.
  3. Ten sam token dwa razy — użytkownik klika „Wyślij” podwójnie; drugi request dostaje timeout-or-duplicate. UI powinno disable’ować przycisk po pierwszym kliknięciu (isSubmitting w React).
  4. Brak action w execute — trudniejsza analiza w panelu Google; zawsze podawaj sensowną nazwę akcji per formularz.
  5. Logowanie pełnego tokena w produkcji — token to dane jednorazowe, ale i tak nie wrzucaj go do publicznych logów ani Sentry breadcrumbs.

Wielojęzyczność (/pl, /en, /de)

Skrypt reCAPTCHA w wspólnym layout.tsx obejmuje wszystkie locale — nie duplikuj kluczy per język. Tekst disclosure bierze się z plików tłumaczeń (messages/*.json), więc każda wersja językowa ma poprawne linki prawne bez hardcodu w komponencie. Komunikaty błędów API (400/429) warto w przyszłości też zlokalizować po locale z body — dziś część jest po polsku niezależnie od /en, co warto poprawić przy refaktorze i18n backendu.

Integracja z e-mailem i CRM

Po udanej weryfikacji reCAPTCHA endpoint /api/submissions zapisuje lead w Prisma i wysyła maile transakcyjne (admin + potwierdzenie dla klienta) przez sendTransactionalEmail. Spam, który przejdzie reCAPTCHA, i tak trafi do skrzynki — stąd rate limit i opcjonalny honeypot przed kosztownym wysyłaniem maili. Score nie musi lądować w CRM; wystarczy log serwerowy z IP i timestampem do późniejszej kalibracji progu. Przy dużym napływie śmieci: podnieś próg score albo filtruj podejrzane domeny na poziomie API — po reCAPTCHA, nie zamiast niego.

Porównanie z innymi metodami antyspamowymi

Metoda Mocna strona Słabość
reCAPTCHA v3 Analiza zachowania, niewidoczne Zależność od Google, wymóg RODO
Honeypot Proste, zero kosztu Łatwe do obejścia przez celowane boty
Rate limit Zatrzymuje flood Nie chroni przy wielu IP
Double opt-in e-mail Jakość adresów Nie blokuje bota przed submit
Akismet / filtry ML Komentarze, blogi Mniej standard w formularzach B2B

Produkcyjny układ w tym projekcie: rate limit + reCAPTCHA v3 + (opcjonalnie) honeypot — trzy warstwy na różne wektory ataku.

FAQ

Czy site key może być w repozytorium?

Tak — site key jest publiczny (widać go w HTML). Secret key nigdy — tylko zmienne środowiskowe serwera.

Czy wystarczy sama weryfikacja po stronie klienta?

Nie. Token można skopiować z DevTools lub wysłać stary. Decyzja musi paść na serwerze po siteverify.

Dlaczego token jest pusty mimo skryptu w layout?

Najczęściej execute przed grecaptcha.ready albo skrypt zablokowany przez adblock. Rozwiązanie: wzorzec z pollingiem i timeoutem jak w page.tsx.

Czy 0.5 to oficjalna rekomendacja Google?

Google podaje interpretację score, nie jeden próg. 0.5 to praktyczny default; kalibruj na swoim ruchu.

reCAPTCHA v3 vs Cloudflare Turnstile?

Turnstile bywa lżejszy i prostszy UX; reCAPTCHA v3 integruje się z ekosystemem Google i wieloma panelami hostingu. Wybór zależy od polityki prywatności i kosztu utrzymania — ważniejsze niż marka jest server-side verify + rate limit.

Czy muszę pokazywać badge?

Tak — regulamin Google. Można stylizować pozycję (jak w projekcie: mała ikona + pełny tekst przy formularzu).

Podsumowanie

Skuteczna ochrona formularza to nie wklejenie skryptu z tutoriala, tylko spójny pipeline: lazyOnload w layout, cierpliwe execute po ready, obowiązkowy siteverify z progiem score, rate limit na API, disclosure RODO przy polu submit, i ten sam schemat dla chatbota. Próg 0.5 to punkt startowy — kalibruj na logach. Przy false positives obniż próg lub dodaj honeypot, zamiast wyłączać ochronę całkowicie.

Chcesz zabezpieczyć formularze u siebie?

  • Skontaktuj się — wdrożymy reCAPTCHA v3, rate limit i audyt antyspamowy pod Twoją stronę
  • Strony WWW — Next.js, formularze leadowe i RODO w jednym pakiecie
  • Zobacz realizacje — produkcyjne strony z niewidoczną ochroną przed botami

O autorze

Budujemy szybkie strony WWW, aplikacje web/mobile, chatboty AI i hosting — z naciskiem na SEO i konwersję.

Przydatne linki

Od teorii do produkcji — Branchly, hosting, opieka i realizacje.

PODOBA CI SIĘ NASZA ARCHITEKTURA MYŚLENIA? ZBUDUJMY COŚ RAZEM.

[ ROZPOCZNIJ_KONFIGURACJĘ_PROJEKTU ]