When Google rolled out v2.0 of their reCAPTCHA service I got a nice email from them reminding me to upgrade. Since I like shiny new things and have a liberty of freely implementing them they needn't ask me twice to do so. As for what's new and shiny in it - they market is as noCAPTCHA reCAPTCHA, meaning no more typing hard to read text from an image we all got used to. You get a nice checkbox-like thing instead, and they are able to tell from the way it's ticked whether it was of human doing for most of the time. When they are not able to tell, you might get tormented to click food pictures instead, which to me happens especially often whenever I decided to skip my lunch. Still better than trying to decipher a cryptic piece of text I think.

The goal of the implementation is to make it as nicely integrated into Spring MVC form validation flow as possible, meaning making it a normal form field that passes through WebDataBinder validation chain. This is also a nice opportunity to learn how to make a call to external API.

For a refresher on how this flow looks like you can read Spring Boot MVC Application tutorial that I've written some time ago. The source code for this article is heavily based on it too.

How it works

So we need an additional widget in the form, and this is how it may look like:

Recaptcha in form

Technically the widget provides an additional form field with a user's response, which should be in turn verified against an API to say whether the response was valid, or was it Skynet doing it.

I will take a bottom-up approach here, so I'll start with preparing back-end for verification, then jump to form validation and finally to the front-end.

Obtaining API keys

To verify a CAPTCHA response we need to access the API, meaning you should generate keys from reCaptcha admin website. You should have received 'site' and 'secret' keys, so let's add them to application.properties, together with service URL used for response verification later:

# Recaptcha
recaptcha.url=https://www.google.com/recaptcha/api/siteverify
recaptcha.site-key=your_site_key
recaptcha.secret-key=your_secret_key

It's good idea to externalise this into a configuration file, especially because since 2.0 the keys are tied to the domain the widget runs on. That's why you'll probably need different keys for development/testing environments. All the keys work for localhost though.

Making API call

So let's assume we have a user's response to a CAPTCHA, so we need a service to validate it against the API. Its interface will be simple:

public interface RecaptchaService {

    boolean isResponseValid(String remoteIp, String response);

}

As you can see it takes user's remote IP address together with response and returns a result of validation (true/false). The IP address is optional, but I imagine that using it can improve the validation. They probably maintain a database of blacklisted IP addresses that are known to belong to robots.

The implementation:

@Service
public class RecaptchaServiceImpl implements RecaptchaService {

    private static class RecaptchaResponse {
        @JsonProperty("success")
        private boolean success;
        @JsonProperty("error-codes")
        private Collection<String> errorCodes;
    }

    private final RestTemplate restTemplate;

    @Value("${recaptcha.url}")
    private String recaptchaUrl;

    @Value("${recaptcha.secret-key}")
    private String recaptchaSecretKey;

    @Autowired
    public RecaptchaServiceImpl(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public boolean isResponseValid(String remoteIp, String response) {
        RecaptchaResponse recaptchaResponse;
        try {
            recaptchaResponse = restTemplate.postForEntity(
                    recaptchaUrl, createBody(recaptchaSecretKey, remoteIp, response), RecaptchaResponse.class)
                    .getBody();
        } catch (RestClientException e) {
            throw new RecaptchaServiceException("Recaptcha API not available due to exception", e);
        }
        return recaptchaResponse.success;
    }

    private MultiValueMap<String, String> createBody(String secret, String remoteIp, String response) {
        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.add("secret", secret);
        form.add("remoteip", remoteIp);
        form.add("response", response);
        return form;
    }

}

Now let's comment on what's happening there:

  • The RecaptchaResponse inner class is a POJO the API response is mapped to. The raw API response is a JSON object like this:

    {
      "success": true|false,
      "error-codes": [...]   // optional
    }
    

    That's why there are @JsonProperty annotations used on the object, so both nodes get mapped to its properties.

  • recaptchaUrl and recaptchaSecretKey get populated from application.properties.

  • The service uses Spring's RestTemplate to make an actual API call. The call is POST with secret, remoteip and response parameters.

  • RestClientException gets caught in case Google fails to pay its energy bills and the API is not responsive. This exception is wrapped into more meaningful RecaptchaServiceException, which is a custom RuntimeException. This is optional, it's just because I don't want to 'leak' the fact that I was using RestTemplate outside of this service. I might want to replace it in the future with something different, like maybe a Java client library provided by Google, who knows.

And that's basically how a HTTP call to any external API you might imagine can be done using Spring.

Configuring RestTemplate

Even with Spring Boot, the RestTemplate needs some configuration first or you'll run into Spring complaining that it cannot inject the bean into RecaptchaServiceImpl.

So in pom.xml I've added a dependency on Apache HTTP Client. If you're not using Spring Boot parent POM, remember to provide an explicit <version> for this to work.

    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>

And a RestTemplateConfig class to set up RestTemplate as a Bean:

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory httpRequestFactory) {
        RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
        return restTemplate;
    }

    @Bean
    public ClientHttpRequestFactory httpRequestFactory(HttpClient httpClient) {
        return new HttpComponentsClientHttpRequestFactory(httpClient);
    }

    @Bean
    public HttpClient httpClient() {
        return HttpClientBuilder.create().build();
    }

}

The default configuration for HTTP client should be sufficient for the example sake. You should be aware that Apache HTTP client is very configurable, and you can make it to support connection pooling too, which can have performance advantages.

Form Data Transfer Object

Since we can now verify a user's response to a CAPTCHA, time to create a form that will hold this response. We'll assume that there may be more forms like this, so let's make an abstract base class they all can share:

public abstract class RecaptchaForm {

  @NotEmpty
  private String recaptchaResponse;

  // getters + setters here

}

The recaptchaResponse property has @NotEmpty validation constraint because we require this field to be present in the form in order to pass validation. It comes from Hibernate Validator, and besides of what @NotNull does it also checks whether the property is an empty string.

Now having this base RecaptchaForm we can use it in different forms, like UserCreateForm in my case:

public class UserCreateForm extends RecaptchaForm {
    // fields specific to the form
}

Since it's really simple, alternatively, you can make an interface out of RecaptchaForm in order to avoid inheritance altogether. Just maybe remember to change its name to something more appropriate for an interface like RecaptchaEnabled, or think of something better.

Validator

Once the form is ready, it will need to be validated using the RecaptchaService. This is where the custom form validators come into play.

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RecaptchaFormValidator implements Validator {

    private static final String ERROR_RECAPTCHA_INVALID = "recaptcha.error.invalid";
    private static final String ERROR_RECAPTCHA_UNAVAILABLE = "recaptcha.error.unavailable";
    private final HttpServletRequest httpServletRequest;
    private final RecaptchaService recaptchaService;

    @Autowired
    public RecaptchaFormValidator(HttpServletRequest httpServletRequest, RecaptchaService recaptchaService) {
        this.httpServletRequest = httpServletRequest;
        this.recaptchaService = recaptchaService;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return RecaptchaForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        RecaptchaForm form = (RecaptchaForm) target;
        try {
            if (form.getRecaptchaResponse() != null
                    && !form.getRecaptchaResponse().isEmpty()
                    && !recaptchaService.isResponseValid(httpServletRequest.getRemoteAddr(), form.getRecaptchaResponse())) {
                errors.reject(ERROR_RECAPTCHA_INVALID);
                errors.rejectValue("recaptchaResponse", ERROR_RECAPTCHA_INVALID);
            }
        } catch (RecaptchaServiceException e) {
            errors.reject(ERROR_RECAPTCHA_UNAVAILABLE);
        }
    }    
}

As a reminder, validator implements a Validator interface, which exposes two methods:

  • supports(...) - this tells to which objects the validator applies. Here I check whether an object is a subclass of RecaptchaForm.

  • validate(...) - this does actual validation. The logic here is to check whether recaptchaResponse property is not empty, and if it's not, it's checked against RecaptchaService. If the validation fails, or exception occurs, then the form field gets rejected with a message.

The validator is a @Component, which makes it an ordinary Spring bean, so we can inject it later and it can have dependencies injected into it too.

Another thing that needs a comment is the @Scope annotation. This makes the validator request scoped. It means that this bean gets created on per-request basis. This is important, since I'm injecting HttpServletRequest into it in order to get user's IP from httpServletRequest.getRemoteAddr() call.

By the way, the IP address returned by httpServletRequest.getRemoteAddr() can bo not a real user's IP address. It all depends on your environment. I very often use a proxy that proxies request from http://somedomain/ to an actual Java application running on http://somelocalmachine:8080/. Since in that case the actual requests made to a Java application come from the proxy, therefore getRemoteAddr() call will return proxy's IP address instead of the user's. Fortunately, most proxies can be configured to pass the real IP address in request headers.

Whenever they are, the following code may be useful to get a real user's IP:

private String getRemoteIp(HttpServletRequest request) {
    String ip = request.getHeader("x-forwarded-for");
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddr();
    }
    return ip;
}

In Spring Boot however, when using embedded Tomcat, it's not required to do it all, as Tomcat can be configured to transparently handle those headers. All that is needed is to add some properties into application.properties:

server.tomcat.remote_ip_header=x-forwarded-for
server.tomcat.protocol_header=x-forwarded-proto

For applications running on standalone Tomcat container the same can be achieved by setting 'Remote IP' Valve in Tomcat configuration.

Controller

Having most of the pieces in place it's time for a controller to handle the form. The methods needed are quite standard. One will handle a GET request, returning an empty form:

@RequestMapping(value = "/user_create.html", method = RequestMethod.GET)
public ModelAndView getCreateUserView() {
    return new ModelAndView("user_create", "form", new UserCreateForm());
}

The other one handles a POST request with the form data:

@RequestMapping(value = "/user_create.html", method = RequestMethod.POST)
public String createUser(@ModelAttribute("form") @Valid UserCreateForm form, BindingResult result) {
    if (result.hasErrors()) {
        return "user_create";
    }

    // do something on success
}

Few comments to remind again on what's this is about:

  • The method takes UserCreateForm instance, which is annotated by @Valid to trigger validation. It's also annotated by @ModelAttribute("form"), which makes the form available to the user_create view as a model property named form.

  • BindingResult is an object that we can 'ask' about whether the validated form has errors. If it does, the view gets rendered again. If not - we can do something to actually process the form, like passing it to the service to save it to a database.

Another thing is to associate RecaptchaFormValidator with the form, so it gets executed during validation. This is done by configuring WebDataBinder in @InitBinder annotated method:

@InitBinder("form")
public void initBinder(WebDataBinder binder) {
    binder.addValidators(recaptchaFormValidator);
}

As you can see WebDataBinder.addValidators() method is called to add validator instance to the list of validators the form will be treated with. The recaptchaFormValidator instance should be injected into the Controller.

One final thing that is needed is that the view should know about our Recaptcha 'site' key, which is later used to render the widget. Since we have it in application.properties it needs to be taken from there, and exposed as a @ModelAttribute, here named recaptchaSiteKey. I've done it using the method:

@ModelAttribute("recaptchaSiteKey")
public String getRecaptchaSiteKey(@Value("${recaptcha.site-key}") String recaptchaSiteKey) {
    return recaptchaSiteKey;
}

As a result, the final controller will look more or less like this:

@Controller
public class UserCreateController {

    private final RecaptchaFormValidator recaptchaFormValidator;

    @ModelAttribute("recaptchaSiteKey")
    public String getRecaptchaSiteKey(@Value("${recaptcha.site-key}") String recaptchaSiteKey) {
        return recaptchaSiteKey;
    }

    @Autowired
    public UserCreateController(RecaptchaFormValidator recaptchaFormValidator) {
        this.recaptchaFormValidator = recaptchaFormValidator;
    }

    @InitBinder("form")
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(recaptchaFormValidator);
    }

    @RequestMapping(value = "/user_create.html", method = RequestMethod.GET)
    public ModelAndView getCreateUserView() {
        // implementation as above
    }

    @RequestMapping(value = "/user_create.html", method = RequestMethod.POST)
    public String createUser(@ModelAttribute("form") @Valid UserCreateForm form, BindingResult result) {
        // implementation as above
    }

}        

View

The view for the form may look like below when using JSP:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<form:form method="post" action="/user_create.html" modelAttribute="form">
    <form:errors path="" element="div" />

    <!-- other form fields here... -->

    <div id="g-recaptcha"></div>
    <form:hidden path="recaptchaResponse"/>
    <script type="text/javascript">
        var onloadCallback = function() {
            grecaptcha.render('g-recaptcha', {
                'sitekey' : '<c:out value="${recaptchaSiteKey}" />',
                'callback' : function(response) {
                    document.getElementById('recaptchaResponse').value = response;
                },
                'theme' : 'light'
            });
        }
    </script>
    <script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
    <form:errors path="recaptchaResponse" class="help-block"/>
    <div>
        <input type="submit" value="<spring:message code="save"/>"/>
    </div>
</form:form>

How it works:

  • The API is included in <script> tag, with parameter onload, which defines a callback to call when it gets loaded. The &render=explicit part says it's up to the client to trigger rendering of the widget.

  • Actual rendering happens in onloadCallback, where the call to grecaptcha.render(...) indicates that the widget should be rendered inside of <div id="g-recaptcha">. It takes a sitekey as a parameter, which is populated from recaptchaSiteKey model attribute supplied by the UserCreateController.

  • There is a callback node also present, which defines a callback function to be called when the user gives a response to the captcha. It's updating a hidden form field recaptchaResponse value with the actual response. This value gets submitted together with the form.

At this point this is practically everything that is needed to have a form secured by a reCAPTCHA v2, so you can safely skip the next section if you're not interested.

g-recaptcha-response binding problem

So as you can see I did some JavaScript gymnastics to populate a hidden form field with the response. This shouldn't be needed, because the default behaviour of reCAPTCHA v2 is to render a form field together with the widget that gets submitted as a part of the form. This is exactly what happens if you use non-explicit way to render the widget:

<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="<c:out value="${recaptchaSiteKey}" />" data-theme="light"></div>

The problem however lies in that the name of the field is g-recaptcha-response, which looks nice, but contains hyphens. This in turn makes it impossible for Spring to bind this request parameter to form field, because it is synthetically not allowed to use hyphenated names in Java as object properties/methods.

One solution to this problem is the one I've chosen on the basis that since reCAPTCHA widget lives in the JavaScript world, so JavaScript solution is the most appropriate to fix it.

In many cases forms are send through JavaScript HTTP request as a JSON data anyways. Then since Jackson has @JsonProperty annotation to map between JSON nodes and object properties, the solution would be to use it in RecaptchaForm:

 public abstract class RecaptchaForm {

   @NotEmpty
   @JsonProperty("g-recaptcha-response")
   private String recaptchaResponse;

   // getters + setters here

 }

For server-side solutions I can imagine creating a Filter such that once it encounters g-recaptcha-response request parameter it would copy/rename it to recaptchaResponse that can be safely bound to a property of RecaptchaForm. Creating a filter is in Spring Boot is simple, all it takes is to implement Filter interface (part of the Servlet API) and make a Spring Bean out of it, i.e. by annotating it as a @Component:

@Component
public class RecaptchaResponseFilter implements Filter {

    private static final String RECAPTCHA_RESPONSE_ALIAS = "recaptchaResponse";
    private static final String RECAPTCHA_RESPONSE_ORIGINAL = "g-recaptcha-response";

    private static class ModifiedHttpServerRequest extends HttpServletRequestWrapper {

        final Map<String, String[]> parameters;

        public ModifiedHttpServerRequest(HttpServletRequest request) {
            super(request);
            parameters = new HashMap<>(request.getParameterMap());
            parameters.put(RECAPTCHA_RESPONSE_ALIAS, request.getParameterValues(RECAPTCHA_RESPONSE_ORIGINAL));
        }

        @Override
        public String getParameter(String name) {
            return parameters.containsKey(name) ? parameters.get(name)[0] : null;
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            return parameters;
        }

        @Override
        public Enumeration<String> getParameterNames() {
            return Collections.enumeration(parameters.keySet());
        }

        @Override
        public String[] getParameterValues(String name) {
            return parameters.get(name);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if (servletRequest instanceof HttpServletRequest
                && servletRequest.getParameter(RECAPTCHA_RESPONSE_ORIGINAL) != null) {
            filterChain.doFilter(new ModifiedHttpServerRequest((HttpServletRequest) servletRequest), servletResponse);
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override
    public void destroy() {
    }

}

Spring Boot should pick it up and add to the filter chain. Trick here is that since we cannot directly modify ServletRequest the solution is to wrap it into HttpServletRequestWrapper and override getParameter...() methods to return what we want. This looks kinda awful, but it's supposedly the way it should be done.

Another solution would be to modify the binding logic by extending ServletModelAttributeMethodProcessor just for that purpose, or even in more generic way to support @JsonProperty-like annotations but for servlet request parameters. There is an example on how to do it on StackOverflow.

Conclussion

Hope it was helpful, though it's a basic matter it was a nice opportunity to tackle some subjects around Spring MVC. Check out the source code for an application that has a CAPTCHA protected form with some Bootstrap eyecandy.

This site uses cookies. By continuing to browse the site, you are agreeing to our use of cookies. Find out more in Privacy and Cookies Policy.