Introduction to reCAPTCHA v3
reCAPTCHA v3 is Google's invisible reCAPTCHA system that returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot) based on user interactions with your site. It works in the background without user interaction.
Table of Contents
- Server-Side Implementation
- Client-Side Integration
- Spring Boot Integration
- Advanced Configuration
- Security Best Practices
- Testing & Monitoring
Server-Side Implementation
Core reCAPTCHA Service
package com.security.recaptcha; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import java.io.IOException; import java.util.Map; public class RecaptchaService { private static final String RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"; private final String secretKey; private final CloseableHttpClient httpClient; private final ObjectMapper objectMapper; private final double scoreThreshold; public RecaptchaService(String secretKey) { this(secretKey, 0.5); // Default threshold } public RecaptchaService(String secretKey, double scoreThreshold) { this.secretKey = secretKey; this.httpClient = HttpClients.createDefault(); this.objectMapper = new ObjectMapper(); this.scoreThreshold = scoreThreshold; } public RecaptchaResponse verify(String recaptchaToken) throws RecaptchaException { return verify(recaptchaToken, null); } public RecaptchaResponse verify(String recaptchaToken, String userIp) throws RecaptchaException { if (recaptchaToken == null || recaptchaToken.trim().isEmpty()) { throw new RecaptchaException("reCAPTCHA token is required"); } try { HttpPost request = new HttpPost(RECAPTCHA_VERIFY_URL); // Build form parameters String params = "secret=" + secretKey + "&response=" + recaptchaToken; if (userIp != null && !userIp.trim().isEmpty()) { params += "&remoteip=" + userIp; } request.setEntity(new StringEntity(params)); request.setHeader("Content-Type", "application/x-www-form-urlencoded"); try (CloseableHttpResponse response = httpClient.execute(request)) { String responseBody = EntityUtils.toString(response.getEntity()); return parseResponse(responseBody); } } catch (IOException e) { throw new RecaptchaException("Failed to verify reCAPTCHA token", e); } } private RecaptchaResponse parseResponse(String responseBody) throws RecaptchaException { try { Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class); boolean success = (Boolean) responseMap.get("success"); double score = responseMap.containsKey("score") ? ((Number) responseMap.get("score")).doubleValue() : 0.0; String action = (String) responseMap.get("action"); String challengeTimestamp = (String) responseMap.get("challenge_ts"); String hostname = (String) responseMap.get("hostname"); @SuppressWarnings("unchecked") Map<String, Object> errorCodes = (Map<String, Object>) responseMap.get("error-codes"); return new RecaptchaResponse(success, score, action, challengeTimestamp, hostname, errorCodes, scoreThreshold); } catch (Exception e) { throw new RecaptchaException("Failed to parse reCAPTCHA response", e); } } public void close() throws IOException { httpClient.close(); } public static class RecaptchaResponse { private final boolean success; private final double score; private final String action; private final String challengeTimestamp; private final String hostname; private final Map<String, Object> errorCodes; private final double scoreThreshold; public RecaptchaResponse(boolean success, double score, String action, String challengeTimestamp, String hostname, Map<String, Object> errorCodes, double scoreThreshold) { this.success = success; this.score = score; this.action = action; this.challengeTimestamp = challengeTimestamp; this.hostname = hostname; this.errorCodes = errorCodes; this.scoreThreshold = scoreThreshold; } public boolean isSuccess() { return success; } public double getScore() { return score; } public String getAction() { return action; } public String getChallengeTimestamp() { return challengeTimestamp; } public String getHostname() { return hostname; } public Map<String, Object> getErrorCodes() { return errorCodes; } public boolean isHuman() { return success && score >= scoreThreshold; } public boolean isBot() { return !success || score < scoreThreshold; } public String getScoreCategory() { if (score >= 0.9) return "VERY_LIKELY_HUMAN"; if (score >= 0.7) return "LIKELY_HUMAN"; if (score >= 0.5) return "POSSIBLE_HUMAN"; if (score >= 0.3) return "SUSPICIOUS"; return "LIKELY_BOT"; } @Override public String toString() { return String.format( "RecaptchaResponse{success=%s, score=%.2f, action=%s, human=%s, category=%s}", success, score, action, isHuman(), getScoreCategory() ); } } public static class RecaptchaException extends Exception { public RecaptchaException(String message) { super(message); } public RecaptchaException(String message, Throwable cause) { super(message, cause); } } } Advanced reCAPTCHA Service with Caching
package com.security.recaptcha; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; public class AdvancedRecaptchaService extends RecaptchaService { private final Cache<String, RecaptchaResponse> responseCache; private final AtomicLong totalRequests; private final AtomicLong failedRequests; private final boolean enableCaching; public AdvancedRecaptchaService(String secretKey, double scoreThreshold, boolean enableCaching) { super(secretKey, scoreThreshold); this.enableCaching = enableCaching; this.totalRequests = new AtomicLong(0); this.failedRequests = new AtomicLong(0); if (enableCaching) { this.responseCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(5, TimeUnit.MINUTES) // Cache for 5 minutes .build(); } else { this.responseCache = null; } } @Override public RecaptchaResponse verify(String recaptchaToken, String userIp) throws RecaptchaException { totalRequests.incrementAndGet(); // Check cache first if (enableCaching && responseCache != null) { String cacheKey = generateCacheKey(recaptchaToken, userIp); RecaptchaResponse cachedResponse = responseCache.getIfPresent(cacheKey); if (cachedResponse != null) { return cachedResponse; } } try { RecaptchaResponse response = super.verify(recaptchaToken, userIp); // Cache successful responses if (enableCaching && responseCache != null && response.isSuccess()) { String cacheKey = generateCacheKey(recaptchaToken, userIp); responseCache.put(cacheKey, response); } return response; } catch (RecaptchaException e) { failedRequests.incrementAndGet(); throw e; } } private String generateCacheKey(String recaptchaToken, String userIp) { return (userIp != null ? userIp + ":" : "") + recaptchaToken; } public RecaptchaStats getStats() { long total = totalRequests.get(); long failed = failedRequests.get(); long success = total - failed; double successRate = total > 0 ? (double) success / total * 100 : 0.0; return new RecaptchaStats(total, success, failed, successRate); } public void clearCache() { if (responseCache != null) { responseCache.invalidateAll(); } } public static class RecaptchaStats { private final long totalRequests; private final long successfulRequests; private final long failedRequests; private final double successRate; public RecaptchaStats(long totalRequests, long successfulRequests, long failedRequests, double successRate) { this.totalRequests = totalRequests; this.successfulRequests = successfulRequests; this.failedRequests = failedRequests; this.successRate = successRate; } // Getters public long getTotalRequests() { return totalRequests; } public long getSuccessfulRequests() { return successfulRequests; } public long getFailedRequests() { return failedRequests; } public double getSuccessRate() { return successRate; } @Override public String toString() { return String.format( "RecaptchaStats{total=%d, success=%d, failed=%d, successRate=%.2f%%}", totalRequests, successfulRequests, failedRequests, successRate ); } } } Client-Side Integration
HTML Template with reCAPTCHA v3
<!DOCTYPE html> <html> <head> <title>reCAPTCHA v3 Demo</title> <script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script> <style> .container { max-width: 500px; margin: 50px auto; padding: 20px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; } button { background: #007cba; color: white; padding: 10px 20px; border: none; cursor: pointer; } button:disabled { background: #ccc; cursor: not-allowed; } .score-display { margin-top: 10px; padding: 10px; border-radius: 4px; } .score-high { background: #d4edda; color: #155724; } .score-medium { background: #fff3cd; color: #856404; } .score-low { background: #f8d7da; color: #721c24; } </style> </head> <body> <div class="container"> <h2>Contact Form with reCAPTCHA v3</h2> <form id="contactForm"> <div class="form-group"> <label for="name">Name:</label> <input type="text" id="name" name="name" required> </div> <div class="form-group"> <label for="email">Email:</label> <input type="email" id="email" name="email" required> </div> <div class="form-group"> <label for="message">Message:</label> <textarea id="message" name="message" rows="5" required></textarea> </div> <button type="submit" id="submitBtn">Send Message</button> <div id="scoreDisplay" class="score-display" style="display: none;"></div> </form> <div id="result" style="margin-top: 20px;"></div> </div> <script> const siteKey = 'YOUR_SITE_KEY'; // Replace with your actual site key // Initialize reCAPTCHA grecaptcha.ready(function() { document.getElementById('contactForm').addEventListener('submit', function(e) { e.preventDefault(); const submitBtn = document.getElementById('submitBtn'); submitBtn.disabled = true; submitBtn.textContent = 'Verifying...'; // Execute reCAPTCHA grecaptcha.execute(siteKey, {action: 'contact'}).then(function(token) { // Add token to form and submit submitFormWithToken(token); }).catch(function(error) { console.error('reCAPTCHA error:', error); showResult('reCAPTCHA verification failed. Please try again.', 'error'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; }); }); }); function submitFormWithToken(token) { const form = document.getElementById('contactForm'); const formData = new FormData(form); formData.append('recaptchaToken', token); fetch('/api/contact', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { showResult('Message sent successfully!', 'success'); displayScore(data.recaptchaScore, data.recaptchaCategory); form.reset(); } else { showResult('Failed to send message: ' + data.message, 'error'); } }) .catch(error => { console.error('Error:', error); showResult('An error occurred. Please try again.', 'error'); }) .finally(() => { const submitBtn = document.getElementById('submitBtn'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; }); } function showResult(message, type) { const resultDiv = document.getElementById('result'); resultDiv.textContent = message; resultDiv.style.color = type === 'success' ? 'green' : 'red'; } function displayScore(score, category) { const scoreDisplay = document.getElementById('scoreDisplay'); scoreDisplay.style.display = 'block'; scoreDisplay.textContent = `reCAPTCHA Score: ${score.toFixed(2)} (${category})`; // Color code based on score scoreDisplay.className = 'score-display '; if (score >= 0.7) { scoreDisplay.classList.add('score-high'); } else if (score >= 0.5) { scoreDisplay.classList.add('score-medium'); } else { scoreDisplay.classList.add('score-low'); } } // Optional: Pre-fetch token on page load for critical actions grecaptcha.ready(function() { grecaptcha.execute(siteKey, {action: 'pageview'}); }); </script> </body> </html> Spring Boot Integration
Spring Boot Configuration
package com.security.recaptcha.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RecaptchaConfig { @Bean @ConfigurationProperties(prefix = "google.recaptcha") public RecaptchaProperties recaptchaProperties() { return new RecaptchaProperties(); } @Bean public RecaptchaService recaptchaService(RecaptchaProperties properties) { return new AdvancedRecaptchaService( properties.getSecretKey(), properties.getScoreThreshold(), properties.isEnableCaching() ); } @ConfigurationProperties(prefix = "google.recaptcha") public static class RecaptchaProperties { private String siteKey; private String secretKey; private double scoreThreshold = 0.5; private boolean enableCaching = true; private boolean enabled = true; // Getters and setters 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 double getScoreThreshold() { return scoreThreshold; } public void setScoreThreshold(double scoreThreshold) { this.scoreThreshold = scoreThreshold; } public boolean isEnableCaching() { return enableCaching; } public void setEnableCaching(boolean enableCaching) { this.enableCaching = enableCaching; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } } Spring Boot Controller
package com.security.recaptcha.controller; import com.security.recaptcha.AdvancedRecaptchaService; import com.security.recaptcha.RecaptchaService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api") public class RecaptchaController { private final RecaptchaService recaptchaService; private final AdvancedRecaptchaService advancedRecaptchaService; public RecaptchaController(RecaptchaService recaptchaService, AdvancedRecaptchaService advancedRecaptchaService) { this.recaptchaService = recaptchaService; this.advancedRecaptchaService = advancedRecaptchaService; } @PostMapping("/verify-recaptcha") public ResponseEntity<Map<String, Object>> verifyRecaptcha( @RequestParam("recaptchaToken") String recaptchaToken, HttpServletRequest request) { Map<String, Object> response = new HashMap<>(); try { String userIp = getClientIp(request); RecaptchaService.RecaptchaResponse recaptchaResponse = recaptchaService.verify(recaptchaToken, userIp); response.put("success", true); response.put("recaptchaScore", recaptchaResponse.getScore()); response.put("recaptchaCategory", recaptchaResponse.getScoreCategory()); response.put("isHuman", recaptchaResponse.isHuman()); response.put("action", recaptchaResponse.getAction()); if (!recaptchaResponse.isHuman()) { response.put("message", "reCAPTCHA verification failed. Please try again."); } return ResponseEntity.ok(response); } catch (RecaptchaService.RecaptchaException e) { response.put("success", false); response.put("message", "reCAPTCHA verification error: " + e.getMessage()); return ResponseEntity.badRequest().body(response); } } @PostMapping("/contact") public ResponseEntity<Map<String, Object>> handleContactForm( @RequestParam("name") String name, @RequestParam("email") String email, @RequestParam("message") String message, @RequestParam("recaptchaToken") String recaptchaToken, HttpServletRequest request) { Map<String, Object> response = new HashMap<>(); try { String userIp = getClientIp(request); RecaptchaService.RecaptchaResponse recaptchaResponse = recaptchaService.verify(recaptchaToken, userIp); if (!recaptchaResponse.isHuman()) { response.put("success", false); response.put("message", "reCAPTCHA verification failed. Score: " + recaptchaResponse.getScore()); response.put("recaptchaScore", recaptchaResponse.getScore()); response.put("recaptchaCategory", recaptchaResponse.getScoreCategory()); return ResponseEntity.badRequest().body(response); } // Process the contact form (send email, save to database, etc.) boolean processed = processContactForm(name, email, message); if (processed) { response.put("success", true); response.put("message", "Message sent successfully"); response.put("recaptchaScore", recaptchaResponse.getScore()); response.put("recaptchaCategory", recaptchaResponse.getScoreCategory()); return ResponseEntity.ok(response); } else { response.put("success", false); response.put("message", "Failed to process contact form"); return ResponseEntity.badRequest().body(response); } } catch (RecaptchaService.RecaptchaException e) { response.put("success", false); response.put("message", "reCAPTCHA verification error: " + e.getMessage()); return ResponseEntity.badRequest().body(response); } } @PostMapping("/login") public ResponseEntity<Map<String, Object>> handleLogin( @RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("recaptchaToken") String recaptchaToken, HttpServletRequest request) { Map<String, Object> response = new HashMap<>(); try { String userIp = getClientIp(request); RecaptchaService.RecaptchaResponse recaptchaResponse = recaptchaService.verify(recaptchaToken, userIp); // Use stricter threshold for login if (recaptchaResponse.getScore() < 0.7) { response.put("success", false); response.put("message", "Suspicious activity detected. Please try again."); response.put("recaptchaScore", recaptchaResponse.getScore()); response.put("recaptchaCategory", recaptchaResponse.getScoreCategory()); return ResponseEntity.badRequest().body(response); } // Process login boolean loginSuccess = authenticateUser(username, password); if (loginSuccess) { response.put("success", true); response.put("message", "Login successful"); response.put("recaptchaScore", recaptchaResponse.getScore()); } else { response.put("success", false); response.put("message", "Invalid credentials"); } return ResponseEntity.ok(response); } catch (RecaptchaService.RecaptchaException e) { response.put("success", false); response.put("message", "Security verification failed"); return ResponseEntity.badRequest().body(response); } } @GetMapping("/recaptcha/stats") public ResponseEntity<?> getRecaptchaStats() { if (advancedRecaptchaService instanceof AdvancedRecaptchaService) { AdvancedRecaptchaService.RecaptchaStats stats = ((AdvancedRecaptchaService) advancedRecaptchaService).getStats(); return ResponseEntity.ok(stats); } return ResponseEntity.ok("Advanced stats not available"); } private String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } private boolean processContactForm(String name, String email, String message) { // Implement your contact form processing logic // Send email, save to database, etc. return true; // Simulate success } private boolean authenticateUser(String username, String password) { // Implement your authentication logic return true; // Simulate success } } Spring Security Integration
package com.security.recaptcha.security; import com.security.recaptcha.RecaptchaService; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class RecaptchaFilter extends OncePerRequestFilter { private final RecaptchaService recaptchaService; private final String[] protectedPaths = {"/api/login", "/api/register", "/api/contact"}; public RecaptchaFilter(RecaptchaService recaptchaService) { this.recaptchaService = recaptchaService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String path = request.getServletPath(); boolean requiresRecaptcha = requiresRecaptcha(path); if (requiresRecaptcha) { String recaptchaToken = request.getParameter("recaptchaToken"); if (recaptchaToken == null || recaptchaToken.trim().isEmpty()) { sendErrorResponse(response, "reCAPTCHA token is required"); return; } try { String userIp = getClientIp(request); RecaptchaService.RecaptchaResponse recaptchaResponse = recaptchaService.verify(recaptchaToken, userIp); if (!recaptchaResponse.isHuman()) { sendErrorResponse(response, "reCAPTCHA verification failed. Score: " + recaptchaResponse.getScore()); return; } // Add recaptcha score to request attributes for later use request.setAttribute("recaptchaScore", recaptchaResponse.getScore()); request.setAttribute("recaptchaAction", recaptchaResponse.getAction()); } catch (RecaptchaService.RecaptchaException e) { sendErrorResponse(response, "reCAPTCHA verification error"); return; } } filterChain.doFilter(request, response); } private boolean requiresRecaptcha(String path) { for (String protectedPath : protectedPaths) { if (path.startsWith(protectedPath)) { return true; } } return false; } private void sendErrorResponse(HttpServletResponse response, String message) throws IOException { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setContentType("application/json"); response.getWriter().write( "{\"success\": false, \"message\": \"" + message + "\"}" ); } private String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } } Advanced Configuration
Action-Based Score Thresholds
package com.security.recaptcha.advanced; import java.util.HashMap; import java.util.Map; public class ActionBasedRecaptchaService extends RecaptchaService { private final Map<String, Double> actionThresholds; public ActionBasedRecaptchaService(String secretKey) { super(secretKey); this.actionThresholds = new HashMap<>(); setupDefaultThresholds(); } private void setupDefaultThresholds() { // Different thresholds for different actions actionThresholds.put("login", 0.7); // Stricter for login actionThresholds.put("register", 0.6); // Medium for registration actionThresholds.put("contact", 0.5); // Standard for contact forms actionThresholds.put("comment", 0.4); // More lenient for comments actionThresholds.put("pageview", 0.3); // Very lenient for page views actionThresholds.put("download", 0.6); // Medium for downloads actionThresholds.put("purchase", 0.8); // Strict for purchases } public void setActionThreshold(String action, double threshold) { actionThresholds.put(action, threshold); } @Override public RecaptchaResponse verify(String recaptchaToken, String userIp) throws RecaptchaException { // First verify with default threshold RecaptchaResponse response = super.verify(recaptchaToken, userIp); // Then apply action-specific threshold String action = response.getAction(); if (action != null && actionThresholds.containsKey(action)) { double actionThreshold = actionThresholds.get(action); return new RecaptchaResponse( response.isSuccess(), response.getScore(), response.getAction(), response.getChallengeTimestamp(), response.getHostname(), response.getErrorCodes(), actionThreshold ); } return response; } public Map<String, Double> getActionThresholds() { return new HashMap<>(actionThresholds); } } Rate Limiting with reCAPTCHA
package com.security.recaptcha.advanced; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class RateLimitedRecaptchaService extends RecaptchaService { private final Cache<String, AtomicInteger> attemptCache; private final int maxAttempts; private final long lockoutDurationMinutes; public RateLimitedRecaptchaService(String secretKey, double scoreThreshold, int maxAttempts, long lockoutDurationMinutes) { super(secretKey, scoreThreshold); this.maxAttempts = maxAttempts; this.lockoutDurationMinutes = lockoutDurationMinutes; this.attemptCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(lockoutDurationMinutes, TimeUnit.MINUTES) .build(); } @Override public RecaptchaResponse verify(String recaptchaToken, String userIp) throws RecaptchaException { // Check rate limiting if (userIp != null && isRateLimited(userIp)) { throw new RecaptchaException("Too many verification attempts. Please try again later."); } RecaptchaResponse response = super.verify(recaptchaToken, userIp); // Record attempt if (userIp != null) { recordAttempt(userIp, response.isHuman()); } return response; } private boolean isRateLimited(String userIp) { AtomicInteger attempts = attemptCache.getIfPresent(userIp); return attempts != null && attempts.get() >= maxAttempts; } private void recordAttempt(String userIp, boolean successful) { AtomicInteger attempts = attemptCache.get(userIp, k -> new AtomicInteger(0)); if (successful) { // Reset counter on successful verification attempts.set(0); } else { // Increment counter on failed verification attempts.incrementAndGet(); } } public RateLimitStats getRateLimitStats(String userIp) { AtomicInteger attempts = attemptCache.getIfPresent(userIp); int attemptCount = attempts != null ? attempts.get() : 0; boolean isBlocked = attemptCount >= maxAttempts; return new RateLimitStats(attemptCount, maxAttempts, isBlocked); } public void resetRateLimit(String userIp) { attemptCache.invalidate(userIp); } public static class RateLimitStats { private final int currentAttempts; private final int maxAttempts; private final boolean isBlocked; public RateLimitStats(int currentAttempts, int maxAttempts, boolean isBlocked) { this.currentAttempts = currentAttempts; this.maxAttempts = maxAttempts; this.isBlocked = isBlocked; } // Getters public int getCurrentAttempts() { return currentAttempts; } public int getMaxAttempts() { return maxAttempts; } public boolean isBlocked() { return isBlocked; } @Override public String toString() { return String.format( "RateLimitStats{attempts=%d/%d, blocked=%s}", currentAttempts, maxAttempts, isBlocked ); } } } Security Best Practices
Security Configuration Manager
package com.security.recaptcha.security; import java.util.HashMap; import java.util.Map; public class RecaptchaSecurityManager { private final Map<String, SecurityPolicy> securityPolicies; public RecaptchaSecurityManager() { this.securityPolicies = new HashMap<>(); initializeDefaultPolicies(); } private void initializeDefaultPolicies() { // Login policy securityPolicies.put("login", new SecurityPolicy(0.7, 5, 15, true)); // Registration policy securityPolicies.put("register", new SecurityPolicy(0.6, 3, 10, true)); // Contact form policy securityPolicies.put("contact", new SecurityPolicy(0.5, 10, 30, false)); // Comment policy securityPolicies.put("comment", new SecurityPolicy(0.4, 20, 60, false)); // Purchase policy securityPolicies.put("purchase", new SecurityPolicy(0.8, 3, 5, true)); } public SecurityPolicy getPolicy(String action) { return securityPolicies.getOrDefault(action, new SecurityPolicy(0.5, 10, 30, false)); } public void updatePolicy(String action, SecurityPolicy policy) { securityPolicies.put(action, policy); } public SecurityAudit createAudit(String action, double score, String userIp) { SecurityPolicy policy = getPolicy(action); boolean passed = score >= policy.getMinScore(); return new SecurityAudit(action, score, userIp, passed, policy); } public static class SecurityPolicy { private final double minScore; private final int maxAttempts; private final int lockoutMinutes; private final boolean strictMode; public SecurityPolicy(double minScore, int maxAttempts, int lockoutMinutes, boolean strictMode) { this.minScore = minScore; this.maxAttempts = maxAttempts; this.lockoutMinutes = lockoutMinutes; this.strictMode = strictMode; } // Getters public double getMinScore() { return minScore; } public int getMaxAttempts() { return maxAttempts; } public int getLockoutMinutes() { return lockoutMinutes; } public boolean isStrictMode() { return strictMode; } } public static class SecurityAudit { private final String action; private final double score; private final String userIp; private final boolean passed; private final SecurityPolicy policy; private final long timestamp; public SecurityAudit(String action, double score, String userIp, boolean passed, SecurityPolicy policy) { this.action = action; this.score = score; this.userIp = userIp; this.passed = passed; this.policy = policy; this.timestamp = System.currentTimeMillis(); } // Getters public String getAction() { return action; } public double getScore() { return score; } public String getUserIp() { return userIp; } public boolean isPassed() { return passed; } public SecurityPolicy getPolicy() { return policy; } public long getTimestamp() { return timestamp; } public String getRiskLevel() { if (score >= 0.8) return "LOW"; if (score >= 0.6) return "MEDIUM"; if (score >= 0.4) return "HIGH"; return "CRITICAL"; } @Override public String toString() { return String.format( "SecurityAudit{action=%s, score=%.2f, risk=%s, passed=%s, ip=%s}", action, score, getRiskLevel(), passed, userIp ); } } } Testing & Monitoring
Test Utilities
package com.security.recaptcha.testing; import com.security.recaptcha.RecaptchaService; public class RecaptchaTestUtils { // Test tokens for different scenarios public static final String VALID_TEST_TOKEN = "test-valid-token"; public static final String BOT_TEST_TOKEN = "test-bot-token"; public static final String INVALID_TEST_TOKEN = "test-invalid-token"; public static final String EMPTY_TOKEN = ""; public static final String NULL_TOKEN = null; public static class MockRecaptchaService extends RecaptchaService { private final Map<String, RecaptchaResponse> mockResponses; public MockRecaptchaService() { super("test-secret-key"); this.mockResponses = new HashMap<>(); setupDefaultResponses(); } private void setupDefaultResponses() { // Valid human response mockResponses.put(VALID_TEST_TOKEN, new RecaptchaResponse(true, 0.9, "test", "2023-01-01T00:00:00Z", "localhost", null, 0.5)); // Bot response mockResponses.put(BOT_TEST_TOKEN, new RecaptchaResponse(true, 0.2, "test", "2023-01-01T00:00:00Z", "localhost", null, 0.5)); // Invalid response mockResponses.put(INVALID_TEST_TOKEN, new RecaptchaResponse(false, 0.0, "test", "2023-01-01T00:00:00Z", "localhost", Map.of("error", "invalid-input-response"), 0.5)); } @Override public RecaptchaResponse verify(String recaptchaToken, String userIp) throws RecaptchaException { if (NULL_TOKEN == recaptchaToken || EMPTY_TOKEN.equals(recaptchaToken)) { throw new RecaptchaException("reCAPTCHA token is required"); } RecaptchaResponse response = mockResponses.get(recaptchaToken); if (response == null) { throw new RecaptchaException("Invalid reCAPTCHA token"); } return response; } public void addMockResponse(String token, RecaptchaResponse response) { mockResponses.put(token, response); } } public static RecaptchaService.RecaptchaResponse createMockResponse( boolean success, double score, String action) { return new RecaptchaService.RecaptchaResponse( success, score, action, "2023-01-01T00:00:00Z", "localhost", null, 0.5 ); } } Monitoring Dashboard
package com.security.recaptcha.monitoring; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class RecaptchaMonitor { private final ConcurrentHashMap<String, ActionStats> actionStats; private final AtomicLong totalVerifications; private final AtomicLong failedVerifications; private final AtomicLong blockedRequests; private final ScheduledExecutorService scheduler; public RecaptchaMonitor() { this.actionStats = new ConcurrentHashMap<>(); this.totalVerifications = new AtomicLong(0); this.failedVerifications = new AtomicLong(0); this.blockedRequests = new AtomicLong(0); this.scheduler = Executors.newScheduledThreadPool(1); startMonitoring(); } private void startMonitoring() { // Schedule periodic reporting scheduler.scheduleAtFixedRate(this::reportMetrics, 1, 1, TimeUnit.MINUTES); } public void recordVerification(String action, double score, boolean success, boolean blocked) { totalVerifications.incrementAndGet(); if (!success) { failedVerifications.incrementAndGet(); } if (blocked) { blockedRequests.incrementAndGet(); } ActionStats stats = actionStats.computeIfAbsent(action, k -> new ActionStats()); stats.recordVerification(score, success, blocked); } public MonitoringStats getStats() { long total = totalVerifications.get(); long failed = failedVerifications.get(); long blocked = blockedRequests.get(); long success = total - failed; double successRate = total > 0 ? (double) success / total * 100 : 0.0; double blockRate = total > 0 ? (double) blocked / total * 100 : 0.0; Map<String, ActionStats> actionStatsCopy = new HashMap<>(actionStats); return new MonitoringStats(total, success, failed, blocked, successRate, blockRate, actionStatsCopy); } private void reportMetrics() { MonitoringStats stats = getStats(); System.out.println("=== reCAPTCHA Monitoring Report ==="); System.out.printf("Total Verifications: %d%n", stats.getTotalVerifications()); System.out.printf("Success Rate: %.2f%%%n", stats.getSuccessRate()); System.out.printf("Block Rate: %.2f%%%n", stats.getBlockRate()); System.out.printf("Failed Verifications: %d%n", stats.getFailedVerifications()); System.out.println("Action Statistics:"); stats.getActionStats().forEach((action, actionStat) -> { System.out.printf(" %s: avgScore=%.2f, verifications=%d%n", action, actionStat.getAverageScore(), actionStat.getTotalVerifications()); }); System.out.println("==================================="); } public void shutdown() { scheduler.shutdown(); try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } } public static class ActionStats { private final AtomicLong totalVerifications = new AtomicLong(0); private final AtomicLong successfulVerifications = new AtomicLong(0); private final AtomicLong blockedVerifications = new AtomicLong(0); private final AtomicDouble totalScore = new AtomicDouble(0.0); public void recordVerification(double score, boolean success, boolean blocked) { totalVerifications.incrementAndGet(); totalScore.addAndGet(score); if (success) { successfulVerifications.incrementAndGet(); } if (blocked) { blockedVerifications.incrementAndGet(); } } public double getAverageScore() { long total = totalVerifications.get(); return total > 0 ? totalScore.get() / total : 0.0; } // Getters public long getTotalVerifications() { return totalVerifications.get(); } public long getSuccessfulVerifications() { return successfulVerifications.get(); } public long getBlockedVerifications() { return blockedVerifications.get(); } } public static class MonitoringStats { private final long totalVerifications; private final long successfulVerifications; private final long failedVerifications; private final long blockedVerifications; private final double successRate; private final double blockRate; private final Map<String, ActionStats> actionStats; public MonitoringStats(long totalVerifications, long successfulVerifications, long failedVerifications, long blockedVerifications, double successRate, double blockRate, Map<String, ActionStats> actionStats) { this.totalVerifications = totalVerifications; this.successfulVerifications = successfulVerifications; this.failedVerifications = failedVerifications; this.blockedVerifications = blockedVerifications; this.successRate = successRate; this.blockRate = blockRate; this.actionStats = actionStats; } // Getters public long getTotalVerifications() { return totalVerifications; } public long getSuccessfulVerifications() { return successfulVerifications; } public long getFailedVerifications() { return failedVerifications; } public long getBlockedVerifications() { return blockedVerifications; } public double getSuccessRate() { return successRate; } public double getBlockRate() { return blockRate; } public Map<String, ActionStats> getActionStats() { return actionStats; } } // Simple atomic double implementation private static class AtomicDouble { private double value; public synchronized void addAndGet(double delta) { value += delta; } public synchronized double get() { return value; } } } Usage Example
Complete Implementation Example
package com.security.recaptcha.demo; import com.security.recaptcha.AdvancedRecaptchaService; import com.security.recaptcha.RecaptchaService; public class RecaptchaDemo { public static void main(String[] args) { // Initialize reCAPTCHA service AdvancedRecaptchaService recaptchaService = new AdvancedRecaptchaService( "your-secret-key-here", // From Google reCAPTCHA admin 0.5, // Score threshold true // Enable caching ); try { // Simulate a verification request String testToken = "test-recaptcha-token-from-client"; String userIp = "192.168.1.100"; RecaptchaService.RecaptchaResponse response = recaptchaService.verify(testToken, userIp); System.out.println("Verification Result: " + response); if (response.isHuman()) { System.out.println("✅ User is human - proceed with action"); } else { System.out.println("❌ User might be a bot - take appropriate action"); } // Print statistics System.out.println("Service Stats: " + recaptchaService.getStats()); } catch (RecaptchaService.RecaptchaException e) { System.err.println("reCAPTCHA verification failed: " + e.getMessage()); } finally { try { recaptchaService.close(); } catch (Exception e) { System.err.println("Error closing reCAPTCHA service: " + e.getMessage()); } } } } Best Practices Summary
- Score Thresholds: Use appropriate thresholds for different actions
- Caching: Cache successful verifications to reduce API calls
- Rate Limiting: Implement rate limiting to prevent abuse
- Monitoring: Monitor verification success rates and scores
- Fallback Strategy: Have a fallback for when reCAPTCHA is unavailable
- Client Integration: Implement proper client-side integration with appropriate actions
- Security: Never expose your secret key in client-side code
- Testing: Use mock services for testing to avoid hitting API limits
This comprehensive reCAPTCHA v3 implementation provides robust bot protection with flexible configuration, monitoring, and integration capabilities for Java applications.