Overview
CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) integration helps protect applications from automated bots and abuse. This guide covers comprehensive CAPTCHA integration with multiple providers and advanced security features.
Architecture Components
1. CAPTCHA Service Abstraction
public interface CaptchaService {
CaptchaResponse generateCaptcha(CaptchaRequest request);
ValidationResult validateCaptcha(String captchaId, String userInput);
boolean isEnabled();
CaptchaType getType();
CaptchaProvider getProvider();
}
public enum CaptchaType {
TEXT,
IMAGE,
AUDIO,
MATH,
RECAPTCHA_V2,
RECAPTCHA_V3,
HCAPTCHA,
TURNSTILE
}
public enum CaptchaProvider {
GOOGLE_RECAPTCHA,
HCAPTCHA,
CLOUDFLARE_TURNSTILE,
INTERNAL,
CUSTOM
}
2. Core CAPTCHA Models
public class CaptchaRequest {
private final String sessionId;
private final String clientIp;
private final CaptchaType type;
private final Map<String, Object> parameters;
private final DifficultyLevel difficulty;
private final Duration timeout;
public CaptchaRequest(String sessionId, String clientIp, CaptchaType type) {
this.sessionId = sessionId;
this.clientIp = clientIp;
this.type = type;
this.parameters = new HashMap<>();
this.difficulty = DifficultyLevel.MEDIUM;
this.timeout = Duration.ofMinutes(5);
}
// Builder methods
public CaptchaRequest withParameter(String key, Object value) {
this.parameters.put(key, value);
return this;
}
public CaptchaRequest withDifficulty(DifficultyLevel difficulty) {
this.difficulty = difficulty;
return this;
}
public CaptchaRequest withTimeout(Duration timeout) {
this.timeout = timeout;
return this;
}
// Getters
public String getSessionId() { return sessionId; }
public String getClientIp() { return clientIp; }
public CaptchaType getType() { return type; }
public Map<String, Object> getParameters() { return Collections.unmodifiableMap(parameters); }
public DifficultyLevel getDifficulty() { return difficulty; }
public Duration getTimeout() { return timeout; }
}
public class CaptchaResponse {
private final String captchaId;
private final CaptchaType type;
private final String challenge;
private final String answer;
private final Instant expiresAt;
private final Map<String, Object> renderData;
private final Map<String, Object> validationData;
public CaptchaResponse(String captchaId, CaptchaType type, String challenge,
String answer, Duration timeout) {
this.captchaId = captchaId;
this.type = type;
this.challenge = challenge;
this.answer = answer;
this.expiresAt = Instant.now().plus(timeout);
this.renderData = new HashMap<>();
this.validationData = new HashMap<>();
}
// Builder methods
public CaptchaResponse withRenderData(String key, Object value) {
this.renderData.put(key, value);
return this;
}
public CaptchaResponse withValidationData(String key, Object value) {
this.validationData.put(key, value);
return this;
}
public boolean isExpired() {
return Instant.now().isAfter(expiresAt);
}
// Getters
public String getCaptchaId() { return captchaId; }
public CaptchaType getType() { return type; }
public String getChallenge() { return challenge; }
public String getAnswer() { return answer; }
public Instant getExpiresAt() { return expiresAt; }
public Map<String, Object> getRenderData() { return Collections.unmodifiableMap(renderData); }
public Map<String, Object> getValidationData() { return Collections.unmodifiableMap(validationData); }
}
public class ValidationResult {
private final boolean valid;
private final String captchaId;
private final String errorCode;
private final String message;
private final double score; // For reCAPTCHA v3
private final Instant validatedAt;
private final Map<String, Object> metadata;
public ValidationResult(boolean valid, String captchaId) {
this.valid = valid;
this.captchaId = captchaId;
this.errorCode = null;
this.message = valid ? "Validation successful" : "Validation failed";
this.score = 1.0;
this.validatedAt = Instant.now();
this.metadata = new HashMap<>();
}
public ValidationResult(boolean valid, String captchaId, String errorCode,
String message, double score) {
this.valid = valid;
this.captchaId = captchaId;
this.errorCode = errorCode;
this.message = message;
this.score = score;
this.validatedAt = Instant.now();
this.metadata = new HashMap<>();
}
// Builder methods
public ValidationResult withMetadata(String key, Object value) {
this.metadata.put(key, value);
return this;
}
// Getters
public boolean isValid() { return valid; }
public String getCaptchaId() { return captchaId; }
public String getErrorCode() { return errorCode; }
public String getMessage() { return message; }
public double getScore() { return score; }
public Instant getValidatedAt() { return validatedAt; }
public Map<String, Object> getMetadata() { return Collections.unmodifiableMap(metadata); }
public static ValidationResult valid(String captchaId) {
return new ValidationResult(true, captchaId);
}
public static ValidationResult invalid(String captchaId, String errorCode) {
return new ValidationResult(false, captchaId, errorCode, "Validation failed", 0.0);
}
public static ValidationResult suspicious(String captchaId, double score) {
return new ValidationResult(false, captchaId, "SUSPICIOUS_SCORE",
"Suspicious activity detected", score);
}
}
public enum DifficultyLevel {
VERY_EASY,
EASY,
MEDIUM,
HARD,
VERY_HARD
}
CAPTCHA Provider Implementations
1. Google reCAPTCHA Integration
@Component
@ConditionalOnProperty(name = "captcha.provider", havingValue = "recaptcha")
public class GoogleRecaptchaService implements CaptchaService {
private final RestTemplate restTemplate;
private final RecaptchaConfig config;
private final ObjectMapper objectMapper;
public GoogleRecaptchaService(RecaptchaConfig config) {
this.config = config;
this.objectMapper = new ObjectMapper();
this.restTemplate = createRestTemplate();
}
@Override
public CaptchaResponse generateCaptcha(CaptchaRequest request) {
String captchaId = UUID.randomUUID().toString();
Map<String, Object> renderData = new HashMap<>();
renderData.put("siteKey", config.getSiteKey());
renderData.put("type", request.getType().name().toLowerCase());
return new CaptchaResponse(captchaId, request.getType(),
"recaptcha_challenge", "", request.getTimeout())
.withRenderData("siteKey", config.getSiteKey())
.withRenderData("theme", config.getTheme())
.withRenderData("size", config.getSize())
.withValidationData("secretKey", config.getSecretKey());
}
@Override
public ValidationResult validateCaptcha(String captchaId, String userResponse) {
try {
if (userResponse == null || userResponse.trim().isEmpty()) {
return ValidationResult.invalid(captchaId, "MISSING_RESPONSE");
}
Map<String, String> params = new HashMap<>();
params.put("secret", config.getSecretKey());
params.put("response", userResponse);
String url = config.getVerifyUrl() + "?" + buildQueryString(params);
ResponseEntity<String> response = restTemplate.postForEntity(url, null, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
return ValidationResult.invalid(captchaId, "VERIFICATION_FAILED");
}
RecaptchaVerifyResponse verifyResponse = objectMapper.readValue(
response.getBody(), RecaptchaVerifyResponse.class);
return processVerificationResponse(captchaId, verifyResponse);
} catch (Exception e) {
return ValidationResult.invalid(captchaId, "VALIDATION_ERROR");
}
}
@Override
public boolean isEnabled() {
return config.isEnabled();
}
@Override
public CaptchaType getType() {
return config.getCaptchaType();
}
@Override
public CaptchaProvider getProvider() {
return CaptchaProvider.GOOGLE_RECAPTCHA;
}
private ValidationResult processVerificationResponse(String captchaId,
RecaptchaVerifyResponse response) {
if (!response.isSuccess()) {
String errorCode = response.getErrorCodes() != null && !response.getErrorCodes().isEmpty() ?
response.getErrorCodes().get(0) : "VERIFICATION_FAILED";
return ValidationResult.invalid(captchaId, errorCode);
}
// For reCAPTCHA v3, check score threshold
if (config.getCaptchaType() == CaptchaType.RECAPTCHA_V3) {
if (response.getScore() < config.getScoreThreshold()) {
return ValidationResult.suspicious(captchaId, response.getScore());
}
}
return ValidationResult.valid(captchaId)
.withMetadata("score", response.getScore())
.withMetadata("action", response.getAction())
.withMetadata("hostname", response.getHostname());
}
private String buildQueryString(Map<String, String> params) {
return params.entrySet().stream()
.map(entry -> entry.getKey() + "=" + encode(entry.getValue()))
.collect(Collectors.joining("&"));
}
private String encode(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
return value;
}
}
private RestTemplate createRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
// Configure timeouts
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
restTemplate.setRequestFactory(factory);
return restTemplate;
}
// reCAPTCHA Response Models
public static class RecaptchaVerifyResponse {
private boolean success;
private String challenge_ts;
private String hostname;
private double score;
private String action;
private List<String> errorCodes;
// Getters and setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getChallenge_ts() { return challenge_ts; }
public void setChallenge_ts(String challenge_ts) { this.challenge_ts = challenge_ts; }
public String getHostname() { return hostname; }
public void setHostname(String hostname) { this.hostname = hostname; }
public double getScore() { return score; }
public void setScore(double score) { this.score = score; }
public String getAction() { return action; }
public void setAction(String action) { this.action = action; }
public List<String> getErrorCodes() { return errorCodes; }
public void setErrorCodes(List<String> errorCodes) { this.errorCodes = errorCodes; }
}
}
@ConfigurationProperties(prefix = "captcha.recaptcha")
public class RecaptchaConfig {
private boolean enabled = true;
private CaptchaType captchaType = CaptchaType.RECAPTCHA_V2;
private String siteKey;
private String secretKey;
private String verifyUrl = "https://www.google.com/recaptcha/api/siteverify";
private double scoreThreshold = 0.5;
private String theme = "light";
private String size = "normal";
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public CaptchaType getCaptchaType() { return captchaType; }
public void setCaptchaType(CaptchaType captchaType) { this.captchaType = captchaType; }
public String getSiteKey() { return siteKey; }
public void setSiteKey(String siteKey) { this.siteKey = siteKey; }
public String getSecretKey() { return secretKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
public String getVerifyUrl() { return verifyUrl; }
public void setVerifyUrl(String verifyUrl) { this.verifyUrl = verifyUrl; }
public double getScoreThreshold() { return scoreThreshold; }
public void setScoreThreshold(double scoreThreshold) { this.scoreThreshold = scoreThreshold; }
public String getTheme() { return theme; }
public void setTheme(String theme) { this.theme = theme; }
public String getSize() { return size; }
public void setSize(String size) { this.size = size; }
}
2. hCaptcha Integration
@Component
@ConditionalOnProperty(name = "captcha.provider", havingValue = "hcaptcha")
public class HCaptchaService implements CaptchaService {
private final RestTemplate restTemplate;
private final HCaptchaConfig config;
private final ObjectMapper objectMapper;
public HCaptchaService(HCaptchaConfig config) {
this.config = config;
this.objectMapper = new ObjectMapper();
this.restTemplate = createRestTemplate();
}
@Override
public CaptchaResponse generateCaptcha(CaptchaRequest request) {
String captchaId = UUID.randomUUID().toString();
return new CaptchaResponse(captchaId, CaptchaType.HCAPTCHA,
"hcaptcha_challenge", "", request.getTimeout())
.withRenderData("siteKey", config.getSiteKey())
.withRenderData("theme", config.getTheme())
.withRenderData("size", config.getSize())
.withValidationData("secretKey", config.getSecretKey());
}
@Override
public ValidationResult validateCaptcha(String captchaId, String userResponse) {
try {
if (userResponse == null || userResponse.trim().isEmpty()) {
return ValidationResult.invalid(captchaId, "MISSING_RESPONSE");
}
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("secret", config.getSecretKey());
params.add("response", userResponse);
ResponseEntity<String> response = restTemplate.postForEntity(
config.getVerifyUrl(), params, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
return ValidationResult.invalid(captchaId, "VERIFICATION_FAILED");
}
HCaptchaVerifyResponse verifyResponse = objectMapper.readValue(
response.getBody(), HCaptchaVerifyResponse.class);
return processVerificationResponse(captchaId, verifyResponse);
} catch (Exception e) {
return ValidationResult.invalid(captchaId, "VALIDATION_ERROR");
}
}
@Override
public boolean isEnabled() {
return config.isEnabled();
}
@Override
public CaptchaType getType() {
return CaptchaType.HCAPTCHA;
}
@Override
public CaptchaProvider getProvider() {
return CaptchaProvider.HCAPTCHA;
}
private ValidationResult processVerificationResponse(String captchaId,
HCaptchaVerifyResponse response) {
if (!response.isSuccess()) {
String errorCode = response.getErrorCodes() != null && !response.getErrorCodes().isEmpty() ?
response.getErrorCodes().get(0) : "VERIFICATION_FAILED";
return ValidationResult.invalid(captchaId, errorCode);
}
return ValidationResult.valid(captchaId)
.withMetadata("hostname", response.getHostname())
.withMetadata("challenge_ts", response.getChallengeTs())
.withMetadata("credit", response.isCredit());
}
private RestTemplate createRestTemplate() {
// Similar to reCAPTCHA implementation
return new RestTemplate();
}
// hCaptcha Response Models
public static class HCaptchaVerifyResponse {
private boolean success;
private String challenge_ts;
private String hostname;
private boolean credit;
private List<String> errorCodes;
// Getters and setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getChallengeTs() { return challenge_ts; }
public void setChallengeTs(String challenge_ts) { this.challenge_ts = challenge_ts; }
public String getHostname() { return hostname; }
public void setHostname(String hostname) { this.hostname = hostname; }
public boolean isCredit() { return credit; }
public void setCredit(boolean credit) { this.credit = credit; }
public List<String> getErrorCodes() { return errorCodes; }
public void setErrorCodes(List<String> errorCodes) { this.errorCodes = errorCodes; }
}
}
@ConfigurationProperties(prefix = "captcha.hcaptcha")
public class HCaptchaConfig {
private boolean enabled = true;
private String siteKey;
private String secretKey;
private String verifyUrl = "https://hcaptcha.com/siteverify";
private String theme = "light";
private String size = "normal";
// Getters and setters
}
3. Internal Text CAPTCHA Service
@Component
@ConditionalOnProperty(name = "captcha.provider", havingValue = "internal")
public class InternalTextCaptchaService implements CaptchaService {
private final TextCaptchaGenerator captchaGenerator;
private final CaptchaStorageService storageService;
private final InternalCaptchaConfig config;
public InternalTextCaptchaService(TextCaptchaGenerator captchaGenerator,
CaptchaStorageService storageService,
InternalCaptchaConfig config) {
this.captchaGenerator = captchaGenerator;
this.storageService = storageService;
this.config = config;
}
@Override
public CaptchaResponse generateCaptcha(CaptchaRequest request) {
String captchaId = UUID.randomUUID().toString();
// Generate CAPTCHA challenge and answer
TextCaptcha challenge = captchaGenerator.generateChallenge(
request.getType(), request.getDifficulty());
// Store CAPTCHA for validation
storageService.storeCaptcha(captchaId, challenge.getAnswer(), request.getTimeout());
return new CaptchaResponse(captchaId, request.getType(),
challenge.getChallenge(), challenge.getAnswer(),
request.getTimeout())
.withRenderData("challengeText", challenge.getChallenge())
.withRenderData("hint", challenge.getHint())
.withValidationData("caseSensitive", challenge.isCaseSensitive());
}
@Override
public ValidationResult validateCaptcha(String captchaId, String userInput) {
try {
if (userInput == null || userInput.trim().isEmpty()) {
return ValidationResult.invalid(captchaId, "MISSING_INPUT");
}
StoredCaptcha storedCaptcha = storageService.getCaptcha(captchaId);
if (storedCaptcha == null) {
return ValidationResult.invalid(captchaId, "CAPTCHA_NOT_FOUND");
}
if (storedCaptcha.isExpired()) {
storageService.removeCaptcha(captchaId);
return ValidationResult.invalid(captchaId, "CAPTCHA_EXPIRED");
}
boolean isValid = validateUserInput(userInput, storedCaptcha);
storageService.removeCaptcha(captchaId);
if (isValid) {
return ValidationResult.valid(captchaId);
} else {
return ValidationResult.invalid(captchaId, "INCORRECT_ANSWER");
}
} catch (Exception e) {
return ValidationResult.invalid(captchaId, "VALIDATION_ERROR");
}
}
@Override
public boolean isEnabled() {
return config.isEnabled();
}
@Override
public CaptchaType getType() {
return config.getDefaultType();
}
@Override
public CaptchaProvider getProvider() {
return CaptchaProvider.INTERNAL;
}
private boolean validateUserInput(String userInput, StoredCaptcha storedCaptcha) {
Map<String, Object> validationData = storedCaptcha.getValidationData();
boolean caseSensitive = (boolean) validationData.getOrDefault("caseSensitive", false);
if (caseSensitive) {
return userInput.equals(storedCaptcha.getAnswer());
} else {
return userInput.equalsIgnoreCase(storedCaptcha.getAnswer());
}
}
}
@Component
public class TextCaptchaGenerator {
private final Random random = new Random();
private final List<CaptchaPattern> patterns;
public TextCaptchaGenerator() {
this.patterns = initializePatterns();
}
public TextCaptcha generateChallenge(CaptchaType type, DifficultyLevel difficulty) {
CaptchaPattern pattern = selectPattern(type, difficulty);
return pattern.generate();
}
private CaptchaPattern selectPattern(CaptchaType type, DifficultyLevel difficulty) {
return patterns.stream()
.filter(p -> p.supportsType(type) && p.supportsDifficulty(difficulty))
.findFirst()
.orElse(patterns.get(0));
}
private List<CaptchaPattern> initializePatterns() {
List<CaptchaPattern> patterns = new ArrayList<>();
// Math challenges
patterns.add(new MathCaptchaPattern());
// Text distortion challenges
patterns.add(new TextDistortionPattern());
// Word-based challenges
patterns.add(new WordCaptchaPattern());
return patterns;
}
// CAPTCHA Pattern Interface
public interface CaptchaPattern {
TextCaptcha generate();
boolean supportsType(CaptchaType type);
boolean supportsDifficulty(DifficultyLevel difficulty);
}
// Math CAPTCHA Implementation
public static class MathCaptchaPattern implements CaptchaPattern {
private final Random random = new Random();
private final String[] operators = {"+", "-", "*"};
@Override
public TextCaptcha generate() {
int num1 = random.nextInt(10) + 1;
int num2 = random.nextInt(10) + 1;
String operator = operators[random.nextInt(operators.length)];
String challenge = String.format("What is %d %s %d?", num1, operator, num2);
int answer = calculateAnswer(num1, num2, operator);
return new TextCaptcha(challenge, String.valueOf(answer),
"Enter the numerical result", false);
}
@Override
public boolean supportsType(CaptchaType type) {
return type == CaptchaType.MATH;
}
@Override
public boolean supportsDifficulty(DifficultyLevel difficulty) {
return difficulty == DifficultyLevel.EASY || difficulty == DifficultyLevel.MEDIUM;
}
private int calculateAnswer(int num1, int num2, String operator) {
switch (operator) {
case "+": return num1 + num2;
case "-": return num1 - num2;
case "*": return num1 * num2;
default: return 0;
}
}
}
// Text Distortion Implementation
public static class TextDistortionPattern implements CaptchaPattern {
private final Random random = new Random();
private final String characters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
@Override
public TextCaptcha generate() {
int length = 6;
StringBuilder challenge = new StringBuilder();
StringBuilder answer = new StringBuilder();
for (int i = 0; i < length; i++) {
char c = characters.charAt(random.nextInt(characters.length()));
challenge.append(c);
answer.append(c);
}
// Apply some distortion for display
String displayText = applyDistortion(challenge.toString());
return new TextCaptcha(displayText, answer.toString(),
"Enter the characters you see", true);
}
@Override
public boolean supportsType(CaptchaType type) {
return type == CaptchaType.TEXT;
}
@Override
public boolean supportsDifficulty(DifficultyLevel difficulty) {
return true; // Supports all difficulty levels
}
private String applyDistortion(String text) {
// Simple distortion - in real implementation, this would generate an image
return text.chars()
.mapToObj(c -> (char) c + " ")
.collect(Collectors.joining())
.trim();
}
}
// Word-based CAPTCHA Implementation
public static class WordCaptchaPattern implements CaptchaPattern {
private final Random random = new Random();
private final String[] words = {
"apple", "banana", "orange", "grape", "melon", "peach", "pear", "berry"
};
@Override
public TextCaptcha generate() {
String word = words[random.nextInt(words.length)];
String challenge = String.format("Enter the word: %s", word.toUpperCase());
return new TextCaptcha(challenge, word,
"Type the word exactly as shown", true);
}
@Override
public boolean supportsType(CaptchaType type) {
return type == CaptchaType.TEXT;
}
@Override
public boolean supportsDifficulty(DifficultyLevel difficulty) {
return difficulty == DifficultyLevel.VERY_EASY || difficulty == DifficultyLevel.EASY;
}
}
}
public class TextCaptcha {
private final String challenge;
private final String answer;
private final String hint;
private final boolean caseSensitive;
public TextCaptcha(String challenge, String answer, String hint, boolean caseSensitive) {
this.challenge = challenge;
this.answer = answer;
this.hint = hint;
this.caseSensitive = caseSensitive;
}
// Getters
public String getChallenge() { return challenge; }
public String getAnswer() { return answer; }
public String getHint() { return hint; }
public boolean isCaseSensitive() { return caseSensitive; }
}
4. CAPTCHA Storage Service
@Component
public class CaptchaStorageService {
private final Map<String, StoredCaptcha> captchaStore;
private final ScheduledExecutorService cleanupScheduler;
public CaptchaStorageService() {
this.captchaStore = new ConcurrentHashMap<>();
this.cleanupScheduler = Executors.newScheduledThreadPool(1);
startCleanupTask();
}
public void storeCaptcha(String captchaId, String answer, Duration timeout) {
StoredCaptcha storedCaptcha = new StoredCaptcha(captchaId, answer, timeout);
captchaStore.put(captchaId, storedCaptcha);
}
public StoredCaptcha getCaptcha(String captchaId) {
return captchaStore.get(captchaId);
}
public void removeCaptcha(String captchaId) {
captchaStore.remove(captchaId);
}
public void cleanupExpiredCaptchas() {
Instant now = Instant.now();
captchaStore.entrySet().removeIf(entry -> entry.getValue().isExpired(now));
}
public int getStoreSize() {
return captchaStore.size();
}
private void startCleanupTask() {
cleanupScheduler.scheduleAtFixedRate(this::cleanupExpiredCaptchas, 1, 1, TimeUnit.HOURS);
}
@PreDestroy
public void shutdown() {
cleanupScheduler.shutdown();
try {
if (!cleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupScheduler.shutdownNow();
}
} catch (InterruptedException e) {
cleanupScheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
public class StoredCaptcha {
private final String captchaId;
private final String answer;
private final Instant createdAt;
private final Instant expiresAt;
private final Map<String, Object> validationData;
public StoredCaptcha(String captchaId, String answer, Duration timeout) {
this.captchaId = captchaId;
this.answer = answer;
this.createdAt = Instant.now();
this.expiresAt = createdAt.plus(timeout);
this.validationData = new HashMap<>();
}
public boolean isExpired() {
return isExpired(Instant.now());
}
public boolean isExpired(Instant referenceTime) {
return referenceTime.isAfter(expiresAt);
}
public Duration getTimeRemaining() {
return Duration.between(Instant.now(), expiresAt);
}
// Getters
public String getCaptchaId() { return captchaId; }
public String getAnswer() { return answer; }
public Instant getCreatedAt() { return createdAt; }
public Instant getExpiresAt() { return expiresAt; }
public Map<String, Object> getValidationData() { return Collections.unmodifiableMap(validationData); }
public void setValidationData(String key, Object value) {
validationData.put(key, value);
}
}
Advanced CAPTCHA Management
1. CAPTCHA Orchestrator
@Component
public class CaptchaOrchestrator {
private final Map<CaptchaProvider, CaptchaService> captchaServices;
private final CaptchaStrategyManager strategyManager;
private final AbuseDetectionService abuseDetector;
private final CaptchaMetricsService metricsService;
public CaptchaOrchestrator(List<CaptchaService> services,
CaptchaStrategyManager strategyManager,
AbuseDetectionService abuseDetector,
CaptchaMetricsService metricsService) {
this.captchaServices = services.stream()
.collect(Collectors.toMap(CaptchaService::getProvider, Function.identity()));
this.strategyManager = strategyManager;
this.abuseDetector = abuseDetector;
this.metricsService = metricsService;
}
public CaptchaResponse generateCaptcha(CaptchaGenerationRequest request) {
try {
// Check if CAPTCHA is needed based on abuse detection
if (!shouldShowCaptcha(request)) {
return CaptchaResponse.bypass();
}
// Select appropriate CAPTCHA strategy
CaptchaStrategy strategy = strategyManager.selectStrategy(request);
CaptchaService service = captchaServices.get(strategy.getProvider());
if (service == null || !service.isEnabled()) {
throw new CaptchaException("CAPTCHA service not available: " + strategy.getProvider());
}
// Generate CAPTCHA
CaptchaRequest captchaRequest = createCaptchaRequest(request, strategy);
CaptchaResponse response = service.generateCaptcha(captchaRequest);
// Record metrics
metricsService.recordCaptchaGenerated(request.getClientIp(),
strategy.getProvider(), response.getType());
return response;
} catch (Exception e) {
metricsService.recordCaptchaError(request.getClientIp(), e);
throw new CaptchaException("Failed to generate CAPTCHA", e);
}
}
public ValidationResult validateCaptcha(CaptchaValidationRequest request) {
try {
CaptchaService service = captchaServices.get(request.getProvider());
if (service == null || !service.isEnabled()) {
return ValidationResult.invalid(request.getCaptchaId(), "SERVICE_UNAVAILABLE");
}
ValidationResult result = service.validateCaptcha(
request.getCaptchaId(), request.getUserInput());
// Update abuse detection
abuseDetector.recordValidationAttempt(request.getClientIp(), result.isValid());
// Record metrics
metricsService.recordCaptchaValidation(request.getClientIp(),
request.getProvider(), result.isValid());
return result;
} catch (Exception e) {
metricsService.recordCaptchaError(request.getClientIp(), e);
return ValidationResult.invalid(request.getCaptchaId(), "VALIDATION_ERROR");
}
}
public boolean isCaptchaRequired(String clientIp, String endpoint) {
return abuseDetector.isCaptchaRequired(clientIp, endpoint);
}
public CaptchaConfig getConfig() {
Map<CaptchaProvider, ProviderConfig> providerConfigs = new HashMap<>();
for (Map.Entry<CaptchaProvider, CaptchaService> entry : captchaServices.entrySet()) {
CaptchaService service = entry.getValue();
providerConfigs.put(entry.getKey(), new ProviderConfig(
service.isEnabled(),
service.getType()
));
}
return new CaptchaConfig(providerConfigs, strategyManager.getActiveStrategies());
}
private boolean shouldShowCaptcha(CaptchaGenerationRequest request) {
// Bypass CAPTCHA for certain conditions
if (request.isTrustedClient()) {
return false;
}
// Check abuse detection
return abuseDetector.isCaptchaRequired(request.getClientIp(), request.getEndpoint());
}
private CaptchaRequest createCaptchaRequest(CaptchaGenerationRequest request,
CaptchaStrategy strategy) {
return new CaptchaRequest(request.getSessionId(), request.getClientIp(), strategy.getType())
.withDifficulty(strategy.getDifficulty())
.withTimeout(strategy.getTimeout())
.withParameter("endpoint", request.getEndpoint())
.withParameter("userAgent", request.getUserAgent());
}
}
// Advanced Request Models
public class CaptchaGenerationRequest {
private final String sessionId;
private final String clientIp;
private final String endpoint;
private final String userAgent;
private final boolean trustedClient;
private final Map<String, Object> context;
public CaptchaGenerationRequest(String sessionId, String clientIp, String endpoint) {
this.sessionId = sessionId;
this.clientIp = clientIp;
this.endpoint = endpoint;
this.userAgent = null;
this.trustedClient = false;
this.context = new HashMap<>();
}
// Builder methods
public CaptchaGenerationRequest withUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public CaptchaGenerationRequest asTrustedClient() {
this.trustedClient = true;
return this;
}
public CaptchaGenerationRequest withContext(String key, Object value) {
this.context.put(key, value);
return this;
}
// Getters
}
public class CaptchaValidationRequest {
private final String captchaId;
private final String userInput;
private final CaptchaProvider provider;
private final String clientIp;
private final String sessionId;
public CaptchaValidationRequest(String captchaId, String userInput,
CaptchaProvider provider, String clientIp) {
this.captchaId = captchaId;
this.userInput = userInput;
this.provider = provider;
this.clientIp = clientIp;
this.sessionId = null;
}
// Builder methods
public CaptchaValidationRequest withSessionId(String sessionId) {
this.sessionId = sessionId;
return this;
}
// Getters
}
public class CaptchaConfig {
private final Map<CaptchaProvider, ProviderConfig> providers;
private final List<CaptchaStrategy> activeStrategies;
public CaptchaConfig(Map<CaptchaProvider, ProviderConfig> providers,
List<CaptchaStrategy> activeStrategies) {
this.providers = providers;
this.activeStrategies = activeStrategies;
}
// Getters
}
public class ProviderConfig {
private final boolean enabled;
private final CaptchaType type;
public ProviderConfig(boolean enabled, CaptchaType type) {
this.enabled = enabled;
this.type = type;
}
// Getters
}
2. Abuse Detection Service
@Component
public class AbuseDetectionService {
private final Map<String, ClientBehavior> clientBehaviors;
private final AbuseDetectionConfig config;
private final ScheduledExecutorService cleanupScheduler;
public AbuseDetectionService(AbuseDetectionConfig config) {
this.clientBehaviors = new ConcurrentHashMap<>();
this.config = config;
this.cleanupScheduler = Executors.newScheduledThreadPool(1);
startCleanupTask();
}
public boolean isCaptchaRequired(String clientIp, String endpoint) {
ClientBehavior behavior = getClientBehavior(clientIp);
// Check rate limits
if (behavior.getRequestCount(endpoint) > config.getMaxRequestsPerMinute()) {
return true;
}
// Check failed CAPTCHA attempts
if (behavior.getFailedCaptchaCount() > config.getMaxFailedCaptchas()) {
return true;
}
// Check suspicious patterns
if (hasSuspiciousPattern(behavior, endpoint)) {
return true;
}
return false;
}
public void recordRequest(String clientIp, String endpoint) {
ClientBehavior behavior = getClientBehavior(clientIp);
behavior.recordRequest(endpoint);
}
public void recordValidationAttempt(String clientIp, boolean successful) {
ClientBehavior behavior = getClientBehavior(clientIp);
if (successful) {
behavior.recordSuccessfulCaptcha();
} else {
behavior.recordFailedCaptcha();
}
}
public AbuseDetectionStats getStats(String clientIp) {
ClientBehavior behavior = clientBehaviors.get(clientIp);
return behavior != null ? behavior.getStats() : new AbuseDetectionStats();
}
public void resetClient(String clientIp) {
clientBehaviors.remove(clientIp);
}
private ClientBehavior getClientBehavior(String clientIp) {
return clientBehaviors.computeIfAbsent(clientIp,
k -> new ClientBehavior(clientIp, config.getTrackingWindow()));
}
private boolean hasSuspiciousPattern(ClientBehavior behavior, String endpoint) {
// Implement pattern detection logic
// - Rapid successive requests
// - Requests to multiple endpoints in short time
// - Unusual user agent patterns
// - Geographic anomalies
return false;
}
private void startCleanupTask() {
cleanupScheduler.scheduleAtFixedRate(this::cleanupOldBehaviors, 1, 1, TimeUnit.HOURS);
}
private void cleanupOldBehaviors() {
Instant cutoff = Instant.now().minus(config.getTrackingWindow());
clientBehaviors.entrySet().removeIf(entry ->
entry.getValue().getLastActivity().isBefore(cutoff));
}
@PreDestroy
public void shutdown() {
cleanupScheduler.shutdown();
}
}
public class ClientBehavior {
private final String clientIp;
private final Duration trackingWindow;
private final Map<String, RequestCounter> endpointCounters;
private int successfulCaptchas;
private int failedCaptchas;
private Instant lastActivity;
public ClientBehavior(String clientIp, Duration trackingWindow) {
this.clientIp = clientIp;
this.trackingWindow = trackingWindow;
this.endpointCounters = new ConcurrentHashMap<>();
this.successfulCaptchas = 0;
this.failedCaptchas = 0;
this.lastActivity = Instant.now();
}
public void recordRequest(String endpoint) {
RequestCounter counter = endpointCounters.computeIfAbsent(
endpoint, k -> new RequestCounter(trackingWindow));
counter.increment();
lastActivity = Instant.now();
}
public void recordSuccessfulCaptcha() {
successfulCaptchas++;
lastActivity = Instant.now();
}
public void recordFailedCaptcha() {
failedCaptchas++;
lastActivity = Instant.now();
}
public int getRequestCount(String endpoint) {
RequestCounter counter = endpointCounters.get(endpoint);
return counter != null ? counter.getCount() : 0;
}
public int getFailedCaptchaCount() {
return failedCaptchas;
}
public Instant getLastActivity() {
return lastActivity;
}
public AbuseDetectionStats getStats() {
int totalRequests = endpointCounters.values().stream()
.mapToInt(RequestCounter::getCount)
.sum();
return new AbuseDetectionStats(clientIp, totalRequests, successfulCaptchas,
failedCaptchas, lastActivity);
}
}
public class RequestCounter {
private final Duration window;
private final Queue<Instant> requests;
public RequestCounter(Duration window) {
this.window = window;
this.requests = new ConcurrentLinkedQueue<>();
}
public void increment() {
Instant now = Instant.now();
requests.add(now);
cleanupOldRequests(now);
}
public int getCount() {
cleanupOldRequests(Instant.now());
return requests.size();
}
private void cleanupOldRequests(Instant referenceTime) {
Instant cutoff = referenceTime.minus(window);
while (!requests.isEmpty() && requests.peek().isBefore(cutoff)) {
requests.poll();
}
}
}
public class AbuseDetectionStats {
private final String clientIp;
private final int totalRequests;
private final int successfulCaptchas;
private final int failedCaptchas;
private final Instant lastActivity;
public AbuseDetectionStats(String clientIp, int totalRequests, int successfulCaptchas,
int failedCaptchas, Instant lastActivity) {
this.clientIp = clientIp;
this.totalRequests = totalRequests;
this.successfulCaptchas = successfulCaptchas;
this.failedCaptchas = failedCaptchas;
this.lastActivity = lastActivity;
}
// Getters
}
@ConfigurationProperties(prefix = "captcha.abuse-detection")
public class AbuseDetectionConfig {
private boolean enabled = true;
private Duration trackingWindow = Duration.ofHours(1);
private int maxRequestsPerMinute = 60;
private int maxFailedCaptchas = 5;
private double suspiciousThreshold = 0.8;
// Getters and setters
}
Spring Boot Integration
1. Auto-Configuration
@Configuration
@EnableConfigurationProperties({CaptchaProperties.class, RecaptchaConfig.class,
HCaptchaConfig.class, InternalCaptchaConfig.class,
AbuseDetectionConfig.class})
public class CaptchaAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CaptchaOrchestrator captchaOrchestrator(List<CaptchaService> captchaServices,
AbuseDetectionService abuseDetectionService) {
CaptchaStrategyManager strategyManager = new CaptchaStrategyManager();
CaptchaMetricsService metricsService = new CaptchaMetricsService();
return new CaptchaOrchestrator(captchaServices, strategyManager,
abuseDetectionService, metricsService);
}
@Bean
@ConditionalOnMissingBean
public AbuseDetectionService abuseDetectionService(AbuseDetectionConfig config) {
return new AbuseDetectionService(config);
}
@Bean
@ConditionalOnMissingBean
public CaptchaStorageService captchaStorageService() {
return new CaptchaStorageService();
}
@Bean
@ConditionalOnMissingBean
public TextCaptchaGenerator textCaptchaGenerator() {
return new TextCaptchaGenerator();
}
@Bean
@ConditionalOnProperty(name = "captcha.providers.recaptcha.enabled", havingValue = "true")
public GoogleRecaptchaService googleRecaptchaService(RecaptchaConfig config) {
return new GoogleRecaptchaService(config);
}
@Bean
@ConditionalOnProperty(name = "captcha.providers.hcaptcha.enabled", havingValue = "true")
public HCaptchaService hCaptchaService(HCaptchaConfig config) {
return new HCaptchaService(config);
}
@Bean
@ConditionalOnProperty(name = "captcha.providers.internal.enabled", havingValue = "true")
public InternalTextCaptchaService internalTextCaptchaService(
TextCaptchaGenerator generator, CaptchaStorageService storageService,
InternalCaptchaConfig config) {
return new InternalTextCaptchaService(generator, storageService, config);
}
@Bean
public CaptchaHealthIndicator captchaHealthIndicator(CaptchaOrchestrator orchestrator) {
return new CaptchaHealthIndicator(orchestrator);
}
}
@Component
public class CaptchaHealthIndicator implements HealthIndicator {
private final CaptchaOrchestrator orchestrator;
public CaptchaHealthIndicator(CaptchaOrchestrator orchestrator) {
this.orchestrator = orchestrator;
}
@Override
public Health health() {
try {
CaptchaConfig config = orchestrator.getConfig();
long enabledProviders = config.getProviders().values().stream()
.filter(ProviderConfig::isEnabled)
.count();
if (enabledProviders > 0) {
return Health.up()
.withDetail("enabledProviders", enabledProviders)
.withDetail("activeStrategies", config.getActiveStrategies().size())
.build();
} else {
return Health.down()
.withDetail("reason", "No CAPTCHA providers enabled")
.build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}
@ConfigurationProperties(prefix = "captcha")
public class CaptchaProperties {
private boolean enabled = true;
private String defaultProvider = "recaptcha";
private Providers providers = new Providers();
// Getters and setters
public static class Providers {
private boolean recaptcha = true;
private boolean hcaptcha = false;
private boolean internal = true;
// Getters and setters
}
}
@ConfigurationProperties(prefix = "captcha.internal")
public class InternalCaptchaConfig {
private boolean enabled = true;
private CaptchaType defaultType = CaptchaType.MATH;
private Duration defaultTimeout = Duration.ofMinutes(5);
// Getters and setters
}
2. REST API Controllers
@RestController
@RequestMapping("/api/captcha")
@Validated
public class CaptchaController {
private final CaptchaOrchestrator captchaOrchestrator;
private final HttpServletRequest httpServletRequest;
public CaptchaController(CaptchaOrchestrator captchaOrchestrator,
HttpServletRequest httpServletRequest) {
this.captchaOrchestrator = captchaOrchestrator;
this.httpServletRequest = httpServletRequest;
}
@PostMapping("/generate")
public ResponseEntity<CaptchaGenerationResponse> generateCaptcha(
@Valid @RequestBody CaptchaGenerateRequest request) {
String clientIp = getClientIp();
CaptchaGenerationRequest generationRequest = new CaptchaGenerationRequest(
request.getSessionId(), clientIp, request.getEndpoint())
.withUserAgent(httpServletRequest.getHeader("User-Agent"))
.withContext("userAgent", httpServletRequest.getHeader("User-Agent"));
CaptchaResponse captchaResponse = captchaOrchestrator.generateCaptcha(generationRequest);
CaptchaGenerationResponse response = new CaptchaGenerationResponse(
captchaResponse.getCaptchaId(),
captchaResponse.getType(),
captchaResponse.getRenderData(),
captchaResponse.getExpiresAt()
);
return ResponseEntity.ok(response);
}
@PostMapping("/validate")
public ResponseEntity<ValidationResponse> validateCaptcha(
@Valid @RequestBody CaptchaValidateRequest request) {
String clientIp = getClientIp();
CaptchaValidationRequest validationRequest = new CaptchaValidationRequest(
request.getCaptchaId(), request.getUserInput(),
request.getProvider(), clientIp)
.withSessionId(request.getSessionId());
ValidationResult result = captchaOrchestrator.validateCaptcha(validationRequest);
ValidationResponse response = new ValidationResponse(
result.isValid(),
result.getErrorCode(),
result.getMessage(),
result.getScore(),
result.getMetadata()
);
return ResponseEntity.ok(response);
}
@GetMapping("/required")
public ResponseEntity<CaptchaRequiredResponse> isCaptchaRequired(
@RequestParam String endpoint) {
String clientIp = getClientIp();
boolean required = captchaOrchestrator.isCaptchaRequired(clientIp, endpoint);
CaptchaRequiredResponse response = new CaptchaRequiredResponse(required, clientIp);
return ResponseEntity.ok(response);
}
@GetMapping("/config")
public ResponseEntity<CaptchaConfig> getConfig() {
CaptchaConfig config = captchaOrchestrator.getConfig();
return ResponseEntity.ok(config);
}
private String getClientIp() {
String xForwardedFor = httpServletRequest.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return httpServletRequest.getRemoteAddr();
}
}
// API Request/Response Models
public class CaptchaGenerateRequest {
@NotBlank
private String sessionId;
@NotBlank
private String endpoint;
// Getters and setters
}
public class CaptchaGenerationResponse {
private final String captchaId;
private final CaptchaType type;
private final Map<String, Object> renderData;
private final Instant expiresAt;
public CaptchaGenerationResponse(String captchaId, CaptchaType type,
Map<String, Object> renderData, Instant expiresAt) {
this.captchaId = captchaId;
this.type = type;
this.renderData = renderData;
this.expiresAt = expiresAt;
}
// Getters
}
public class CaptchaValidateRequest {
@NotBlank
private String captchaId;
@NotBlank
private String userInput;
@NotNull
private CaptchaProvider provider;
private String sessionId;
// Getters and setters
}
public class ValidationResponse {
private final boolean valid;
private final String errorCode;
private final String message;
private final double score;
private final Map<String, Object> metadata;
public ValidationResponse(boolean valid, String errorCode, String message,
double score, Map<String, Object> metadata) {
this.valid = valid;
this.errorCode = errorCode;
this.message = message;
this.score = score;
this.metadata = metadata;
}
// Getters
}
public class CaptchaRequiredResponse {
private final boolean required;
private final String clientIp;
public CaptchaRequiredResponse(boolean required, String clientIp) {
this.required = required;
this.clientIp = clientIp;
}
// Getters
}
Frontend Integration
1. CAPTCHA Renderer Service
@Service
public class CaptchaRendererService {
private final CaptchaOrchestrator captchaOrchestrator;
private final TemplateEngine templateEngine;
public CaptchaRendererService(CaptchaOrchestrator captchaOrchestrator) {
this.captchaOrchestrator = captchaOrchestrator;
this.templateEngine = createTemplateEngine();
}
public String renderCaptchaWidget(String sessionId, String endpoint, String clientIp) {
CaptchaGenerationRequest request = new CaptchaGenerationRequest(
sessionId, clientIp, endpoint);
CaptchaResponse captchaResponse = captchaOrchestrator.generateCaptcha(request);
Map<String, Object> model = new HashMap<>();
model.put("captcha", captchaResponse);
model.put("sessionId", sessionId);
model.put("endpoint", endpoint);
return templateEngine.process("captcha-widget",
new Context(Locale.getDefault(), model));
}
public String renderRecaptchaV2(String siteKey) {
Map<String, Object> model = new HashMap<>();
model.put("siteKey", siteKey);
return templateEngine.process("recaptcha-v2",
new Context(Locale.getDefault(), model));
}
public String renderRecaptchaV3(String siteKey, String action) {
Map<String, Object> model = new HashMap<>();
model.put("siteKey", siteKey);
model.put("action", action);
return templateEngine.process("recaptcha-v3",
new Context(Locale.getDefault(), model));
}
public String renderHCaptcha(String siteKey) {
Map<String, Object> model = new HashMap<>();
model.put("siteKey", siteKey);
return templateEngine.process("hcaptcha",
new Context(Locale.getDefault(), model));
}
public String renderInternalCaptcha(TextCaptcha captcha) {
Map<String, Object> model = new HashMap<>();
model.put("challenge", captcha.getChallenge());
model.put("hint", captcha.getHint());
model.put("caseSensitive", captcha.isCaseSensitive());
return templateEngine.process("internal-captcha",
new Context(Locale.getDefault(), model));
}
private TemplateEngine createTemplateEngine() {
TemplateEngine templateEngine = new TemplateEngine();
// Configure template resolver (Thymeleaf, Freemarker, etc.)
return templateEngine;
}
}
Configuration
1. Application Configuration
captcha:
enabled: true
default-provider: recaptcha
providers:
recaptcha: true
hcaptcha: false
internal: true
captcha.recaptcha:
enabled: true
captcha-type: RECAPTCHA_V2
site-key: ${RECAPTCHA_SITE_KEY:}
secret-key: ${RECAPTCHA_SECRET_KEY:}
verify-url: https://www.google.com/recaptcha/api/siteverify
score-threshold: 0.5
theme: light
size: normal
captcha.hcaptcha:
enabled: false
site-key: ${HCAPTCHA_SITE_KEY:}
secret-key: ${HCAPTCHA_SECRET_KEY:}
verify-url: https://hcaptcha.com/siteverify
theme: light
size: normal
captcha.internal:
enabled: true
default-type: MATH
default-timeout: 5m
captcha.abuse-detection:
enabled: true
tracking-window: 1h
max-requests-per-minute: 60
max-failed-captchas: 5
suspicious-threshold: 0.8
management:
endpoints:
web:
exposure:
include: "health,captcha"
endpoint:
captcha:
enabled: true
logging:
level:
com.example.captcha: DEBUG
Exception Handling
public class CaptchaException extends RuntimeException {
public CaptchaException(String message) {
super(message);
}
public CaptchaException(String message, Throwable cause) {
super(message, cause);
}
}
@ControllerAdvice
public class CaptchaExceptionHandler {
@ExceptionHandler(CaptchaException.class)
public ResponseEntity<ErrorResponse> handleCaptchaException(CaptchaException e) {
ErrorResponse error = new ErrorResponse(
"CAPTCHA_ERROR",
e.getMessage(),
Instant.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException e) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; "));
ErrorResponse error = new ErrorResponse(
"VALIDATION_ERROR",
errorMessage,
Instant.now()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}
public class ErrorResponse {
private final String errorCode;
private final String message;
private final Instant timestamp;
public ErrorResponse(String errorCode, String message, Instant timestamp) {
this.errorCode = errorCode;
this.message = message;
this.timestamp = timestamp;
}
// Getters
}
Conclusion
CAPTCHA Integration in Java provides:
- Multi-Provider Support: Google reCAPTCHA, hCaptcha, and internal CAPTCHA systems
- Advanced Abuse Detection: Intelligent CAPTCHA triggering based on user behavior
- Flexible Strategies: Configurable CAPTCHA types and difficulty levels
- Comprehensive Metrics: Monitoring and analytics for CAPTCHA performance
- Enterprise Ready: Scalable, configurable, and extensible architecture
Key benefits:
- Bot Protection: Effective defense against automated attacks and abuse
- User Experience: Intelligent CAPTCHA triggering minimizes user friction
- Compliance Ready: Meets accessibility and privacy requirements
- High Performance: Optimized with caching and efficient algorithms
- Security Focused: Comprehensive validation and abuse prevention
This CAPTCHA integration enables organizations to effectively protect their applications from automated threats while maintaining a positive user experience through intelligent CAPTCHA strategies and advanced abuse detection.