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")); } }
Download
From:一号门
COMMENTS