reCAPTCHA v3 Implementation in Java

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

  1. Server-Side Implementation
  2. Client-Side Integration
  3. Spring Boot Integration
  4. Advanced Configuration
  5. Security Best Practices
  6. 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

  1. Score Thresholds: Use appropriate thresholds for different actions
  2. Caching: Cache successful verifications to reduce API calls
  3. Rate Limiting: Implement rate limiting to prevent abuse
  4. Monitoring: Monitor verification success rates and scores
  5. Fallback Strategy: Have a fallback for when reCAPTCHA is unavailable
  6. Client Integration: Implement proper client-side integration with appropriate actions
  7. Security: Never expose your secret key in client-side code
  8. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper