Integrate Google ReCaptcha Java Spring Web Application
This tutorial demonstrates how to integrate Google ReCaptcha into a Java Spring Web Application. reCAPTCHA
is used to verify if the current computer is a human, preventing bots from automatically submitting forms. We integrated Google ReCaptcha using server side validation. We wrote a custom @ReCaptcha
annotation which you can annotate your java fields. This’ll automatically handle the ReCaptcha server side validation process. At the bottom we also wrote some Unit and Integration tests using Mockito
, spring-test
and MockMvc
.
Maven Dependencies
We use Apache Maven to manage our project dependencies. Make sure the following dependencies reside on the class-path.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.memorynotfound.spring.security</groupId>
<artifactId>recaptcha</artifactId>
<version>1.0.0-SNAPSHOT</version>
<url>http://memorynotfound.com</url>
<name>Spring Security - ${project.artifactId}</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- bootstrap and jquery -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.2.1</version>
</dependency>
<!-- testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Google ReCaptcha Settings
First, you need to request a google recaptcha account key and secret before you can start using the service. After you submitted your project, you’ll receive a key
and secret
. Add these in the application.yml
property file below which is located in the src/main/resources
folder.
# =============================================
# = Google Recaptcha configurations
# = https://www.google.com/recaptcha/admin#list
# =============================================
google:
recaptcha:
url: https://www.google.com/recaptcha/api/siteverify
key: <enter-key-here>
secret: <enter-secret-here>
# =============================================
# = Logging configurations
# =============================================
logging:
level:
root: WARN
com.memorynotfound: DEBUG
org.springframework.web: INFO
org.springframework.security: INFO
Next, create a CaptchaSettings
class which is used to map the properties located in the applicaiton.yml
to. Annotate the class using the @ConfigurationProperties
annotation and Spring Boot automatically maps the property to the object. You can read more over this in the Spring Boot @ConfigurationProperties Annotation Example tutorial.
package com.memorynotfound.spring.security.recaptcha;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "google.recaptcha")
public class CaptchaSettings {
private String url;
private String key;
private String secret;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
}
Spring MVC RestTemplate Configuration
Since we are validating the reCAPTCHA
server side, we need to communicate to the google api in order to validate the token. We used the RestTemplate
which we configure using the Apache HttpClient
.
package com.memorynotfound.spring.security.config;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory httpRequestFactory) {
RestTemplate template = new RestTemplate(httpRequestFactory);
template.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
return template;
}
@Bean
public ClientHttpRequestFactory httpRequestFactory(HttpClient httpClient) {
return new HttpComponentsClientHttpRequestFactory(httpClient);
}
@Bean
public HttpClient httpClient() {
return HttpClientBuilder.create().build();
}
}
Server Side Google ReCaptcha Validation
We need to validate the reCAPTCHA
code received from the front-end component server-side. We need to make a request to https://www.google.com/recaptcha/api/siteverify?secret=???&response=???&remoteip=???
and fill in the correct arguments obtained from the CaptchaSettings
class which we created earlier. This’ll return a JSON response that’ll map to the ReCaptchaResponse
class that we create next. Based on the result we pass the validation.
Note: If the validation process fails with an exception, we currently ignore and log it. We can optionally trigger some alerting here or contact the administrator. We do not want the process to fail when the recaptcha service isn’t available.
package com.memorynotfound.spring.security.recaptcha;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestOperations;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
@Service
public class ReCaptchaService {
private static final Logger log = LoggerFactory.getLogger(ReCaptchaService.class);
@Autowired
private RestOperations restTemplate;
@Autowired
private CaptchaSettings captchaSettings;
@Autowired
private HttpServletRequest request;
public boolean validate(String reCaptchaResponse){
URI verifyUri = URI.create(String.format(
captchaSettings.getUrl() + "?secret=%s&response=%s&remoteip=%s",
captchaSettings.getSecret(),
reCaptchaResponse,
request.getRemoteAddr()
));
try {
ReCaptchaResponse response = restTemplate.getForObject(verifyUri, ReCaptchaResponse.class);
return response.isSuccess();
} catch (Exception ignored){
log.error("", ignored);
// ignore when google services are not available
// maybe add some sort of logging or trigger that'll alert the administrator
}
return true;
}
}
The ReCaptchaResponse
is used to map the response received from the google reCAPTCHA API.
package com.memorynotfound.spring.security.recaptcha;
import com.fasterxml.jackson.annotation.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonPropertyOrder({
"success",
"challenge_ts",
"hostname",
"error-codes"
})
public class ReCaptchaResponse {
@JsonProperty("success")
private boolean success;
@JsonProperty("challenge_ts")
private Date challengeTs;
@JsonProperty("hostname")
private String hostname;
@JsonProperty("error-codes")
private ErrorCode[] errorCodes;
@JsonIgnore
public boolean hasClientError() {
ErrorCode[] errors = getErrorCodes();
if(errors == null) {
return false;
}
for(ErrorCode error : errors) {
switch(error) {
case InvalidResponse:
case MissingResponse:
return true;
}
}
return false;
}
static enum ErrorCode {
MissingSecret, InvalidSecret,
MissingResponse, InvalidResponse;
private static Map<String, ErrorCode> errorsMap = new HashMap<>(4);
static {
errorsMap.put("missing-input-secret", MissingSecret);
errorsMap.put("invalid-input-secret", InvalidSecret);
errorsMap.put("missing-input-response", MissingResponse);
errorsMap.put("invalid-input-response", InvalidResponse);
}
@JsonCreator
public static ErrorCode forValue(String value) {
return errorsMap.get(value.toLowerCase());
}
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public Date getChallengeTs() {
return challengeTs;
}
public void setChallengeTs(Date challengeTs) {
this.challengeTs = challengeTs;
}
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public ErrorCode[] getErrorCodes() {
return errorCodes;
}
public void setErrorCodes(ErrorCode[] errorCodes) {
this.errorCodes = errorCodes;
}
}
Creating ReCaptcha Field Annotation
Let’s create a custom @ValidCaptcha
annotation. This is a field-level annotation which we can use to annotate a Java property.
package com.memorynotfound.spring.security.recaptcha;
import javax.validation.Payload;
import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = ReCaptchaConstraintValidator.class)
@Target({ TYPE, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ValidReCaptcha {
String message() default "Invalid ReCaptcha";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The ReCaptchaConstraintValidator
class is responsible for validating the input received from the annotated property.
package com.memorynotfound.spring.security.recaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ReCaptchaConstraintValidator implements ConstraintValidator<ValidReCaptcha, String> {
@Autowired
private ReCaptchaService reCaptchaService;
@Override
public void initialize(ValidReCaptcha constraintAnnotation) {
}
@Override
public boolean isValid(String reCaptchaResponse, ConstraintValidatorContext context) {
if (reCaptchaResponse == null || reCaptchaResponse.isEmpty()){
return true;
}
return reCaptchaService.validate(reCaptchaResponse);
}
}
Google ReCaptcha Request Parameter Problem
By default spring cannot map request parameters with hyphens. And since the google reCAPTCHA
plugin returns the token inside the g-recaptcha-response
request parameter, we need a way to solve this problem.
We opted to write a custom Filter
which checks if the request contains the g-recaptcha-response
request parameter and renames the request parameter to reCaptchaResponse
without the hyphens.
package com.memorynotfound.spring.security.recaptcha;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
@Component
public class ReCaptchaResponseFilter implements Filter {
private static final String RE_CAPTCHA_ALIAS = "reCaptchaResponse";
private static final String RE_CAPTCHA_RESPONSE = "g-recaptcha-response";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getParameter(RE_CAPTCHA_RESPONSE) != null) {
ReCaptchaHttpServletRequest reCaptchaRequest = new ReCaptchaHttpServletRequest(request);
chain.doFilter(reCaptchaRequest, response);
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
private static class ReCaptchaHttpServletRequest extends HttpServletRequestWrapper {
final Map<String, String[]> params;
ReCaptchaHttpServletRequest(HttpServletRequest request) {
super(request);
params = new HashMap<>(request.getParameterMap());
params.put(RE_CAPTCHA_ALIAS, request.getParameterValues(RE_CAPTCHA_RESPONSE));
}
@Override
public String getParameter(String name) {
return params.containsKey(name) ? params.get(name)[0] : null;
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(params.keySet());
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
}
}
Validating Form Submission
We created the ForgotPasswordForm
to map the incoming form request parameters.
We can use this class to validate the incoming form parameters. We used the @ValidCaptcha
annotation – which we created earlier – to automatically validate if the reCAPTCHA
code sent from the client is valid.
package com.memorynotfound.spring.security.web.dto;
import com.memorynotfound.spring.security.recaptcha.ValidReCaptcha;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
public class ForgotPasswordForm {
@Email
@NotEmpty
private String email;
@NotEmpty
@ValidReCaptcha
private String reCaptchaResponse;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getReCaptchaResponse() {
return reCaptchaResponse;
}
public void setReCaptchaResponse(String reCaptchaResponse) {
this.reCaptchaResponse = reCaptchaResponse;
}
}
Submitting Form Controller
We created a simple controller which processes the form and automatically validates the ForgotPasswordForm
using the @Valid
annotation.
package com.memorynotfound.spring.security.web;
import com.memorynotfound.spring.security.web.dto.ForgotPasswordForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.validation.Valid;
@Controller
@RequestMapping("/forgot-password")
public class ForgotPasswordController {
@ModelAttribute("forgotPasswordForm")
public ForgotPasswordForm forgotPasswordForm() {
return new ForgotPasswordForm();
}
@GetMapping
public String showForgotPassword(Model model) {
return "forgot-password";
}
@PostMapping
public String handleForgotPassword(@ModelAttribute("forgotPasswordForm") @Valid ForgotPasswordForm form,
BindingResult result){
if (result.hasErrors()){
return "forgot-password";
}
return "redirect:/forgot-password?success";
}
}
Spring Boot
We use Spring Boot to start our application.
package com.memorynotfound.spring.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Run {
public static void main(String[] args) {
SpringApplication.run(Run.class, args);
}
}
Integrate Google ReCaptcha in Web Application
You need to add the following javascript to your page.
<script src='https://www.google.com/recaptcha/api.js'></script>
Place the google reCAPTCHA
code inside your form.
<div class="g-recaptcha" data-sitekey="<enter-key-here>"></div>
Here is an example forgot-password.html
page which is located in the src/main/resources/templates/
folder.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}"/>
<title>Registration</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-body">
<div class="text-center">
<h3><i class="glyphicon glyphicon-lock" style="font-size:2em;"></i></h3>
<h2 class="text-center">Forgot Password?</h2>
<p>Enter your e-mail address and we'll send you a link to reset your password.</p>
<div class="panel-body">
<div th:if="${param.success}">
<div class="alert alert-info">
You successfully requested a new password!
</div>
</div>
<form th:action="@{/forgot-password}" th:object="${forgotPasswordForm}" method="post">
<div class="form-group"
th:classappend="${#fields.hasErrors('email')}? 'has-error':''">
<div class="input-group">
<span class="input-group-addon">@</span>
<input id="email"
class="form-control"
placeholder="E-mail"
th:field="*{email}"/>
</div>
<p class="error-message"
th:each="error: ${#fields.errors('email')}"
th:text="${error}">Validation error</p>
</div>
<div class="form-group">
<div class="g-recaptcha"
th:attr="data-sitekey=${@captchaSettings.getKey()}"></div>
<p class="error-message"
th:each="error: ${#fields.errors('reCaptchaResponse')}"
th:text="${error}">Validation error</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success btn-block">Register</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" th:src="@{/webjars/jquery/3.2.1/jquery.min.js/}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js"></script>
</body>
</html>
Demo
Access the http://localhost:8080/forgot-password
URL.
Integration Testing
To test our custom reCAPTCHA implementation we wrote some integration tests using Mockito
to mock the ReCaptchaService
, MockMvc
to make http form requests.
package com.memorynotfound.spring.security.test;
import com.memorynotfound.spring.security.recaptcha.ReCaptchaService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.mockito.Mockito.mock;
@Configuration
public class SpringTestConfig {
@Bean
ReCaptchaService reCaptchaService() {
return mock(ReCaptchaService.class);
}
}
We validate if the @ValidCaptcha
annotation is triggering the validation.
package com.memorynotfound.spring.security.test;
import com.memorynotfound.spring.security.recaptcha.ReCaptchaService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@RunWith(SpringJUnit4ClassRunner.class)
public class UserRegistrationIT {
@Autowired
private MockMvc mockMvc;
@Autowired
private ReCaptchaService reCaptchaService;
@Test
public void submitWithoutReCaptcha() throws Exception {
this.mockMvc
.perform(
post("/forgot-password")
.param("email", "[email protected]")
)
.andExpect(model().hasErrors())
.andExpect(model().attributeHasFieldErrors("forgotPasswordForm", "reCaptchaResponse"))
.andExpect(status().isOk());
}
@Test
public void submitWithInvalidReCaptcha() throws Exception {
String invalidReCaptcha = "invalid-re-captcha";
when(reCaptchaService.validate(invalidReCaptcha)).thenReturn(false);
this.mockMvc
.perform(
post("/forgot-password")
.param("email", "[email protected]")
.param("reCaptchaResponse", invalidReCaptcha)
)
.andExpect(model().hasErrors())
.andExpect(model().attributeHasFieldErrors("forgotPasswordForm", "reCaptchaResponse"))
.andExpect(status().isOk());
}
@Test
public void submitWithValidReCaptcha() throws Exception {
String validReCaptcha = "valid-re-captcha";
when(reCaptchaService.validate(validReCaptcha)).thenReturn(true);
this.mockMvc
.perform(
post("/forgot-password")
.param("email", "[email protected]")
.param("reCaptchaResponse", validReCaptcha)
)
.andExpect(model().hasNoErrors())
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/forgot-password?success"));
}
}
References
- Spring Boot – @ConfigurationProperties Annotation Example
- Request Google reCAPTCHA api key
- Google reCAPTCHA developer documentation