reCAPTCHA son dos mitades: un widget en el formulario, que es HTML, y una comprobación en el servidor, que es la parte de Java y la única que de verdad protege. Sin la segunda, el widget es decoración.
También te puede interesar
Las dos claves
En la consola de administración de reCAPTCHA se registra el dominio y se obtienen dos claves:
| Clave | Dónde va | Se puede ver |
|---|---|---|
| De sitio (site key) | en el HTML, a la vista | sí, es pública |
| Secreta (secret key) | solo en el servidor | nunca |
1. El widget, en el formulario
<html>
<head>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body>
<form action="/registro" method="POST">
<input type="text" name="nombre" required>
<input type="email" name="correo" required>
<div class="g-recaptcha" data-sitekey="TU_CLAVE_DE_SITIO"></div>
<button type="submit">Enviar</button>
</form>
</body>
</html>El <div class="g-recaptcha"> es donde aparece la casilla «no soy un robot». Al enviar el formulario, el navegador añade un campo oculto llamado g-recaptcha-response con un testigo. Ese testigo es lo que hay que comprobar.
2. La verificación, en el servidor
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public class Recaptcha {
private static final HttpClient CLIENTE = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)).build();
static boolean verificar(String secreto, String respuesta, String ip) throws Exception {
String cuerpo = "secret=" + enc(secreto) + "&response=" + enc(respuesta)
+ (ip == null ? "" : "&remoteip=" + enc(ip));
HttpRequest pet = HttpRequest.newBuilder()
.uri(URI.create("https://www.google.com/recaptcha/api/siteverify"))
.header("Content-Type", "application/x-www-form-urlencoded")
.timeout(Duration.ofSeconds(15))
.POST(HttpRequest.BodyPublishers.ofString(cuerpo))
.build();
HttpResponse<String> r = CLIENTE.send(pet, HttpResponse.BodyHandlers.ofString());
return r.body().contains("\"success\": true");
}
private static String enc(String s) {
return URLEncoder.encode(s, StandardCharsets.UTF_8);
}
}Esto es con el HttpClient del JDK, sin dependencias. Y así responde Google cuando el testigo no vale —la salida es de una llamada de verdad al servicio, con una clave inventada—:
salida{
"success": false,
"error-codes": [
"invalid-input-response"
]
}Para analizar la respuesta en serio conviene Gson en vez de buscar un texto dentro de la cadena:
record RespuestaCaptcha(boolean success,
String challenge_ts,
String hostname,
java.util.List<String> errorCodes) {}
var datos = new Gson().fromJson(r.body(), RespuestaCaptcha.class);
if (!datos.success()) {
log.warn("captcha rechazado: {}", datos.errorCodes());
}En un servlet
@Override
protected void doPost(HttpServletRequest pet, HttpServletResponse res)
throws ServletException, IOException {
String testigo = pet.getParameter("g-recaptcha-response");
if (testigo == null || testigo.isBlank()
|| !Recaptcha.verificar(System.getenv("RECAPTCHA_SECRET"), testigo, pet.getRemoteAddr())) {
res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Verifica que no eres un robot");
return;
}
// a partir de aquí, el formulario es de fiar
}Los códigos de error
| Código | Qué ha pasado |
|---|---|
missing-input-secret | no mandaste la clave secreta |
invalid-input-secret | la clave secreta es incorrecta o no es la del dominio |
missing-input-response | no llegó el testigo: el usuario no marcó la casilla |
invalid-input-response | el testigo es falso, está caducado o ya se usó |
timeout-or-duplicate | el testigo ya se había validado, o pasaron más de 2 minutos |
timeout-or-duplicate, casi siempre es porque has reenviado el mismo formulario o porque estás verificando dos veces la misma respuesta.Dos cosas que conviene tener claras
La comprobación va siempre en el servidor. El widget se puede saltar; el testigo no, porque solo Google sabe si es válido y solo tu clave secreta puede preguntárselo. Validar en JavaScript no protege de nada: quien envía el formulario con un programa se salta tu JavaScript entero.
Y conviene no depender solo del captcha. Si Google no responde, tu formulario deja de funcionar: hay que decidir de antemano qué hacer con el tiempo de espera agotado. Un campo trampa invisible y un límite de envíos por IP hacen bastante trabajo, no envían datos de tus usuarios a nadie y no se caen.
Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.