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.

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

上一篇: Field Matching Bean Validation Annotation Example
下一篇: Custom Password Constraint Validator Annotation Example
 评论 ( What Do You Think )
名称
邮箱
网址
评论
验证
   
 

 


  • 微信公众号

  • 我的微信

站点声明:

1、一号门博客CMS,由Python, MySQL, Nginx, Wsgi 强力驱动

2、部分文章或者资源来源于互联网, 有时候很难判断是否侵权, 若有侵权, 请联系邮箱:summer@yihaomen.com, 同时欢迎大家注册用户,主动发布无版权争议的 文章/资源.

3、鄂ICP备14001754号-3, 鄂公网安备 42280202422812号