├── .gitignore ├── README.md ├── bower.json ├── gruntfile.js ├── package.json ├── pom.xml └── src └── main ├── java └── eu │ └── kielczewski │ └── example │ ├── Application.java │ ├── config │ └── RestTemplateConfig.java │ ├── controller │ ├── UserCreateController.java │ ├── UserListController.java │ └── UserRestController.java │ ├── domain │ ├── User.java │ └── form │ │ ├── RecaptchaForm.java │ │ └── UserCreateForm.java │ ├── filter │ └── RecaptchaResponseFilter.java │ ├── repository │ └── UserRepository.java │ ├── service │ ├── recaptcha │ │ ├── RecaptchaService.java │ │ ├── RecaptchaServiceImpl.java │ │ └── exception │ │ │ └── RecaptchaServiceException.java │ └── user │ │ ├── UserService.java │ │ ├── UserServiceImpl.java │ │ └── exception │ │ └── UserAlreadyExistsException.java │ └── validator │ ├── RecaptchaFormValidator.java │ └── UserCreateFormPasswordValidator.java ├── resources ├── application.properties ├── data.sql └── messages.properties └── webapp └── WEB-INF ├── jsp ├── user_create.jsp └── user_list.jsp └── tags └── layout.tag /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | bower_components 5 | node_modules 6 | **/webapp/public -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Example Spring Boot Application using reCAPTCHA v2 2 | ================================================== 3 | 4 | Blog article available at [http://kielczewski.eu/2015/07/spring-recaptcha-v2-form-validation](http://kielczewski.eu/2015/07/spring-recaptcha-v2-form-validation). 5 | 6 | Requirements 7 | ------------ 8 | * [Java Platform (JDK) 8](http://www.oracle.com/technetwork/java/javase/downloads/index.html) 9 | * [Apache Maven 3.x](http://maven.apache.org/) 10 | 11 | Quick start 12 | ----------- 13 | 1. `mvn spring-boot:run` 14 | 2. Point your browser to [http://localhost:8080/](http://localhost:8080/) -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-spring-boot", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "bootstrap": "~3.3", 6 | "angular": "~1.4", 7 | "angular-resource": "~1.4", 8 | "angular-bootstrap": "~0.13" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | var bowerDir = 'bower_components/'; 4 | var srcDir = 'src/main/webapp/WEB-INF/'; 5 | var lessDir = srcDir + 'less/'; 6 | var jsDir = srcDir + 'js/'; 7 | var imgDir = srcDir + 'images/'; 8 | 9 | var dstDir = 'src/main/webapp/public/'; 10 | var dstCssDir = dstDir + 'css/'; 11 | var dstJsDir = dstDir + 'js/'; 12 | var dstImgDir = dstDir + 'images/'; 13 | var dstFontDir = dstDir + 'fonts/'; 14 | 15 | grunt.initConfig({ 16 | pkg: grunt.file.readJSON('package.json'), 17 | 18 | clean: [dstDir], 19 | 20 | watch: { 21 | files: [ 22 | jsDir + '**', 23 | lessDir + '**', 24 | imgDir + '**' 25 | ], 26 | tasks: 'dev' 27 | }, 28 | 29 | copy: { 30 | bootstrap_css_map: { 31 | expand: true, 32 | cwd: bowerDir + 'bootstrap/dist/css', 33 | src: ['bootstrap.css.map'], 34 | dest: dstCssDir 35 | }, 36 | bootstrap_fonts: { 37 | expand: true, 38 | cwd: bowerDir + 'bootstrap/fonts', 39 | src: ['*'], 40 | dest: dstFontDir 41 | }, 42 | images_css: { 43 | expand: true, 44 | cwd: lessDir, 45 | src: ['images/*'], 46 | dest: dstCssDir 47 | }, 48 | images: { 49 | expand: true, 50 | cwd: imgDir, 51 | src: [ '**/*' ], 52 | dest: dstImgDir 53 | } 54 | }, 55 | 56 | uglify: { 57 | dist: { 58 | options: { 59 | compress: { 60 | warnings: true 61 | }, 62 | report: 'min' 63 | }, 64 | src: [ 65 | bowerDir + 'angular/angular.js', 66 | bowerDir + 'angular-resource/angular-resource.js', 67 | bowerDir + 'angular-bootstrap/ui-bootstrap.js', 68 | bowerDir + 'angular-bootstrap/ui-bootstrap-tpls.js', 69 | jsDir + '*.js', 70 | jsDir + '**/*.js' 71 | ], 72 | dest: dstJsDir + 'all.js' 73 | }, 74 | dev: { 75 | options: { 76 | beautify: true, 77 | compress: false, 78 | report: 'min' 79 | }, 80 | src: '<%= uglify.dist.src %>', 81 | dest: '<%= uglify.dist.dest %>' 82 | } 83 | }, 84 | 85 | less: { 86 | dist: { 87 | options: { 88 | compress: true, 89 | yuicompress: true, 90 | optimization: 2, 91 | report: 'min' 92 | }, 93 | src: [ 94 | bowerDir + 'bootstrap/dist/css/bootstrap.css', 95 | lessDir + '*.less' 96 | ], 97 | dest: dstCssDir + 'all.css' 98 | }, 99 | dev: { 100 | options: { 101 | compress: false, 102 | yuicompress: false, 103 | report: 'min' 104 | }, 105 | src: '<%= less.dist.src %>', 106 | dest: '<%= less.dist.dest %>' 107 | } 108 | }, 109 | 110 | compress: { 111 | dist: { 112 | options: { 113 | mode: 'gzip' 114 | }, 115 | files: [ 116 | {expand: true, cwd: dstJsDir, src: ['*.js', '**/*.js'], dest: dstJsDir, ext: '.js.gz'}, 117 | {expand: true, cwd: dstCssDir, src: ['*.css', '**/*.css'], dest: dstCssDir, ext: '.css.gz'} 118 | ] 119 | } 120 | } 121 | }); 122 | 123 | grunt.loadNpmTasks('grunt-contrib-clean'); 124 | grunt.loadNpmTasks('grunt-contrib-watch'); 125 | grunt.loadNpmTasks('grunt-contrib-uglify'); 126 | grunt.loadNpmTasks('grunt-contrib-less'); 127 | grunt.loadNpmTasks('grunt-contrib-copy'); 128 | grunt.loadNpmTasks('grunt-contrib-compress'); 129 | 130 | grunt.registerTask('default', ['uglify:dist', 'less:dist', 'copy', 'compress']); 131 | grunt.registerTask('dev', ['uglify:dev', 'less:dev', 'copy', 'compress']); 132 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-sp", 3 | "version": "0.1.0", 4 | "devDependencies": { 5 | "bower": "~1.4", 6 | "grunt": "~0.4", 7 | "grunt-cli": "~0.1.13", 8 | "grunt-contrib-clean": "~0.4", 9 | "grunt-contrib-watch": "~0.6", 10 | "grunt-contrib-uglify": "~0.9", 11 | "grunt-contrib-less": "~1.0", 12 | "grunt-contrib-copy": "~0.4", 13 | "grunt-contrib-compress": "~0.13" 14 | } 15 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | eu.kielczewski.example.spring 6 | example-spring-boot 7 | 1.0-SNAPSHOT 8 | war 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 1.2.5.RELEASE 14 | 15 | 16 | Example Spring Boot Application 17 | 18 | 19 | 1.7 20 | 18.0 21 | UTF-8 22 | UTF-8 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-test 37 | test 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-data-jpa 48 | 49 | 50 | 51 | 52 | 53 | org.apache.tomcat.embed 54 | tomcat-embed-jasper 55 | provided 56 | 57 | 58 | 59 | 60 | 61 | javax.servlet 62 | jstl 63 | 64 | 65 | 66 | 67 | 68 | org.apache.httpcomponents 69 | httpclient 70 | 71 | 72 | 73 | 74 | 75 | org.hibernate 76 | hibernate-validator 77 | 78 | 79 | 80 | 81 | 82 | org.hsqldb 83 | hsqldb 84 | runtime 85 | 86 | 87 | 88 | 89 | 90 | com.google.guava 91 | guava 92 | ${guava.version} 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.springframework.boot 105 | spring-boot-maven-plugin 106 | 107 | 108 | 109 | 110 | 111 | pl.allegro 112 | grunt-maven-plugin 113 | 1.5.1 114 | 115 | node_modules/.bin/bower 116 | node_modules/.bin/grunt 117 | true 118 | WEB-INF 119 | ${project.basedir} 120 | 121 | 122 | 123 | 124 | npm 125 | bower 126 | grunt 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/Application.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.builder.SpringApplicationBuilder; 7 | import org.springframework.boot.context.web.SpringBootServletInitializer; 8 | import org.springframework.context.annotation.ComponentScan; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @SpringBootApplication 12 | public class Application extends SpringBootServletInitializer { 13 | 14 | public static void main(final String[] args) { 15 | SpringApplication.run(Application.class, args); 16 | } 17 | 18 | @Override 19 | protected final SpringApplicationBuilder configure(final SpringApplicationBuilder application) { 20 | return application.sources(Application.class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.config; 2 | 3 | import org.apache.http.client.HttpClient; 4 | import org.apache.http.impl.client.HttpClientBuilder; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.client.ClientHttpRequestFactory; 8 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 9 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import java.util.concurrent.TimeUnit; 13 | 14 | @Configuration 15 | public class RestTemplateConfig { 16 | 17 | @Bean 18 | public RestTemplate restTemplate(ClientHttpRequestFactory httpRequestFactory) { 19 | RestTemplate restTemplate = new RestTemplate(httpRequestFactory); 20 | restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter()); 21 | return restTemplate; 22 | } 23 | 24 | @Bean 25 | public ClientHttpRequestFactory httpRequestFactory(HttpClient httpClient) { 26 | return new HttpComponentsClientHttpRequestFactory(httpClient); 27 | } 28 | 29 | @Bean 30 | public HttpClient httpClient() { 31 | return HttpClientBuilder.create().build(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/controller/UserCreateController.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.controller; 2 | 3 | import eu.kielczewski.example.domain.User; 4 | import eu.kielczewski.example.domain.form.UserCreateForm; 5 | import eu.kielczewski.example.service.user.UserService; 6 | import eu.kielczewski.example.service.user.exception.UserAlreadyExistsException; 7 | import eu.kielczewski.example.validator.RecaptchaFormValidator; 8 | import eu.kielczewski.example.validator.UserCreateFormPasswordValidator; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.validation.BindingResult; 15 | import org.springframework.web.bind.WebDataBinder; 16 | import org.springframework.web.bind.annotation.InitBinder; 17 | import org.springframework.web.bind.annotation.ModelAttribute; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RequestMethod; 20 | import org.springframework.web.servlet.ModelAndView; 21 | 22 | import javax.validation.Valid; 23 | 24 | @Controller 25 | public class UserCreateController { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(UserCreateController.class); 28 | private final UserService userService; 29 | private final UserCreateFormPasswordValidator passwordValidator; 30 | private final RecaptchaFormValidator recaptchaFormValidator; 31 | 32 | @Autowired 33 | public UserCreateController(UserService userService, UserCreateFormPasswordValidator passwordValidator, 34 | RecaptchaFormValidator recaptchaFormValidator) { 35 | this.userService = userService; 36 | this.passwordValidator = passwordValidator; 37 | this.recaptchaFormValidator = recaptchaFormValidator; 38 | } 39 | 40 | @InitBinder("form") 41 | public void initBinder(WebDataBinder binder) { 42 | binder.addValidators(passwordValidator); 43 | binder.addValidators(recaptchaFormValidator); 44 | } 45 | 46 | @ModelAttribute("recaptchaSiteKey") 47 | public String getRecaptchaSiteKey(@Value("${recaptcha.site-key}") String recaptchaSiteKey) { 48 | return recaptchaSiteKey; 49 | } 50 | 51 | @RequestMapping(value = "/user_create.html", method = RequestMethod.GET) 52 | public ModelAndView getCreateUserView() { 53 | LOGGER.debug("Received request for user_create view"); 54 | return new ModelAndView("user_create", "form", new UserCreateForm()); 55 | } 56 | 57 | @RequestMapping(value = "/user_create.html", method = RequestMethod.POST) 58 | public String createUser(@ModelAttribute("form") @Valid UserCreateForm form, BindingResult result) { 59 | LOGGER.debug("Received request to user_create view, form={}, result={}", form, result); 60 | if (result.hasErrors()) { 61 | return "user_create"; 62 | } 63 | try { 64 | userService.save(new User(form.getId(), form.getPassword2())); 65 | } catch (UserAlreadyExistsException e) { 66 | LOGGER.debug("Tried to create user with existing id", e); 67 | result.rejectValue("id", "user.error.id.exists"); 68 | return "user_create"; 69 | } 70 | return "redirect:/user_list.html"; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/controller/UserListController.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.controller; 2 | 3 | import eu.kielczewski.example.service.user.UserService; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.ModelMap; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.servlet.ModelAndView; 11 | 12 | @Controller 13 | public class UserListController { 14 | 15 | private static final Logger LOGGER = LoggerFactory.getLogger(UserListController.class); 16 | private final UserService userService; 17 | 18 | @Autowired 19 | public UserListController(final UserService userService) { 20 | this.userService = userService; 21 | } 22 | 23 | @RequestMapping("/user_list.html") 24 | public ModelAndView getListUsersView() { 25 | LOGGER.debug("Received request to get user list view"); 26 | ModelMap model = new ModelMap(); 27 | model.addAttribute("users", userService.getList()); 28 | return new ModelAndView("user_list", model); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/controller/UserRestController.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.controller; 2 | 3 | import eu.kielczewski.example.domain.User; 4 | import eu.kielczewski.example.service.user.UserService; 5 | import eu.kielczewski.example.service.user.exception.UserAlreadyExistsException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import javax.validation.Valid; 13 | import java.util.List; 14 | 15 | @RestController 16 | public class UserRestController { 17 | 18 | private static final Logger LOGGER = LoggerFactory.getLogger(UserRestController.class); 19 | private final UserService userService; 20 | 21 | @Autowired 22 | public UserRestController(final UserService userService) { 23 | this.userService = userService; 24 | } 25 | 26 | @RequestMapping(value = "/user", method = RequestMethod.POST) 27 | public User createUser(@RequestBody @Valid final User user) { 28 | LOGGER.debug("Received request to create the {}", user); 29 | return userService.save(user); 30 | } 31 | 32 | @RequestMapping(value = "/user", method = RequestMethod.GET) 33 | public List listUsers() { 34 | LOGGER.debug("Received request to list all users"); 35 | return userService.getList(); 36 | } 37 | 38 | @ExceptionHandler 39 | @ResponseStatus(HttpStatus.CONFLICT) 40 | public String handleUserAlreadyExistsException(UserAlreadyExistsException e) { 41 | return e.getMessage(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/domain/User.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Id; 6 | import javax.validation.constraints.NotNull; 7 | import javax.validation.constraints.Size; 8 | 9 | @Entity 10 | public class User { 11 | 12 | @Id 13 | @NotNull 14 | @Size(max = 64) 15 | @Column(name = "id", nullable = false, updatable = false) 16 | private String id; 17 | 18 | @NotNull 19 | @Size(max = 64) 20 | @Column(name = "password", nullable = false) 21 | private String password; 22 | 23 | User() { 24 | } 25 | 26 | public User(final String id, final String password) { 27 | this.id = id; 28 | this.password = password; 29 | } 30 | 31 | public String getId() { 32 | return id; 33 | } 34 | 35 | public String getPassword() { 36 | return password; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return "User{" + 42 | "id='" + id + '\'' + 43 | ", password='" + password + '\'' + 44 | '}'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/domain/form/RecaptchaForm.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.domain.form; 2 | 3 | import org.hibernate.validator.constraints.NotEmpty; 4 | import org.springframework.web.bind.annotation.ModelAttribute; 5 | 6 | public abstract class RecaptchaForm { 7 | 8 | @NotEmpty 9 | private String recaptchaResponse; 10 | 11 | public void setRecaptchaResponse(String response) { 12 | this.recaptchaResponse = response; 13 | } 14 | 15 | public String getRecaptchaResponse() { 16 | return recaptchaResponse; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "RecaptchaForm{" + 22 | "recaptchaResponse='" + recaptchaResponse + '\'' + 23 | '}'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/domain/form/UserCreateForm.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.domain.form; 2 | 3 | import org.hibernate.validator.constraints.NotEmpty; 4 | 5 | import javax.validation.constraints.Size; 6 | 7 | public class UserCreateForm extends RecaptchaForm { 8 | 9 | @NotEmpty 10 | @Size(min = 3, max = 64) 11 | private String id; 12 | @NotEmpty 13 | @Size(min = 8, max = 64) 14 | private String password1; 15 | private String password2; 16 | 17 | public String getId() { 18 | return id; 19 | } 20 | 21 | public void setId(String id) { 22 | this.id = id; 23 | } 24 | 25 | public String getPassword1() { 26 | return password1; 27 | } 28 | 29 | public void setPassword1(String password1) { 30 | this.password1 = password1; 31 | } 32 | 33 | public String getPassword2() { 34 | return password2; 35 | } 36 | 37 | public void setPassword2(String password2) { 38 | this.password2 = password2; 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return "UserCreateForm{" + 44 | "id='" + id + '\'' + 45 | ", password1='" + password1 + '\'' + 46 | ", password2='" + password2 + '\'' + 47 | "} " + super.toString(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/filter/RecaptchaResponseFilter.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.filter; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import javax.servlet.*; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletRequestWrapper; 8 | import java.io.IOException; 9 | import java.util.Collections; 10 | import java.util.Enumeration; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | // Uncomment the following to see how the filter might work 15 | //@Component 16 | public class RecaptchaResponseFilter implements Filter { 17 | 18 | private static final String RECAPTCHA_RESPONSE_ALIAS = "recaptchaResponse"; 19 | private static final String RECAPTCHA_RESPONSE_ORIGINAL = "g-recaptcha-response"; 20 | 21 | private static class ModifiedHttpServerRequest extends HttpServletRequestWrapper { 22 | 23 | final Map parameters; 24 | 25 | public ModifiedHttpServerRequest(HttpServletRequest request) { 26 | super(request); 27 | parameters = new HashMap<>(request.getParameterMap()); 28 | parameters.put(RECAPTCHA_RESPONSE_ALIAS, request.getParameterValues(RECAPTCHA_RESPONSE_ORIGINAL)); 29 | } 30 | 31 | @Override 32 | public String getParameter(String name) { 33 | return parameters.containsKey(name) ? parameters.get(name)[0] : null; 34 | } 35 | 36 | @Override 37 | public Map getParameterMap() { 38 | return parameters; 39 | } 40 | 41 | @Override 42 | public Enumeration getParameterNames() { 43 | return Collections.enumeration(parameters.keySet()); 44 | } 45 | 46 | @Override 47 | public String[] getParameterValues(String name) { 48 | return parameters.get(name); 49 | } 50 | } 51 | 52 | @Override 53 | public void init(FilterConfig filterConfig) throws ServletException { 54 | } 55 | 56 | @Override 57 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 58 | if (servletRequest instanceof HttpServletRequest 59 | && servletRequest.getParameter(RECAPTCHA_RESPONSE_ORIGINAL) != null) { 60 | filterChain.doFilter(new ModifiedHttpServerRequest((HttpServletRequest) servletRequest), servletResponse); 61 | } else { 62 | filterChain.doFilter(servletRequest, servletResponse); 63 | } 64 | } 65 | 66 | @Override 67 | public void destroy() { 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.repository; 2 | 3 | import eu.kielczewski.example.domain.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface UserRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/service/recaptcha/RecaptchaService.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.service.recaptcha; 2 | 3 | public interface RecaptchaService { 4 | 5 | boolean isResponseValid(String remoteIp, String response); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/service/recaptcha/RecaptchaServiceImpl.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.service.recaptcha; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import eu.kielczewski.example.service.recaptcha.exception.RecaptchaServiceException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.util.LinkedMultiValueMap; 11 | import org.springframework.util.MultiValueMap; 12 | import org.springframework.web.client.RestClientException; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | import java.util.Collection; 16 | 17 | @Service 18 | public class RecaptchaServiceImpl implements RecaptchaService { 19 | 20 | private static class RecaptchaResponse { 21 | @JsonProperty("success") 22 | private boolean success; 23 | @JsonProperty("error-codes") 24 | private Collection errorCodes; 25 | 26 | @Override 27 | public String toString() { 28 | return "RecaptchaResponse{" + 29 | "success=" + success + 30 | ", errorCodes=" + errorCodes + 31 | '}'; 32 | } 33 | } 34 | 35 | private static final Logger LOGGER = LoggerFactory.getLogger(RecaptchaServiceImpl.class); 36 | private final RestTemplate restTemplate; 37 | 38 | @Value("${recaptcha.url}") 39 | private String recaptchaUrl; 40 | 41 | @Value("${recaptcha.secret-key}") 42 | private String recaptchaSecretKey; 43 | 44 | @Autowired 45 | public RecaptchaServiceImpl(RestTemplate restTemplate) { 46 | this.restTemplate = restTemplate; 47 | } 48 | 49 | @Override 50 | public boolean isResponseValid(String remoteIp, String response) { 51 | LOGGER.debug("Validating captcha response for remoteIp={}, response={}", remoteIp, response); 52 | RecaptchaResponse recaptchaResponse; 53 | try { 54 | recaptchaResponse = restTemplate.postForEntity( 55 | recaptchaUrl, createBody(recaptchaSecretKey, remoteIp, response), RecaptchaResponse.class) 56 | .getBody(); 57 | } catch (RestClientException e) { 58 | throw new RecaptchaServiceException("Recaptcha API not available exception", e); 59 | } 60 | if (recaptchaResponse.success) { 61 | return true; 62 | } else { 63 | LOGGER.debug("Unsuccessful recaptchaResponse={}", recaptchaResponse); 64 | return false; 65 | } 66 | } 67 | 68 | private MultiValueMap createBody(String secret, String remoteIp, String response) { 69 | MultiValueMap form = new LinkedMultiValueMap<>(); 70 | form.add("secret", secret); 71 | form.add("remoteip", remoteIp); 72 | form.add("response", response); 73 | return form; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/service/recaptcha/exception/RecaptchaServiceException.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.service.recaptcha.exception; 2 | 3 | public class RecaptchaServiceException extends RuntimeException { 4 | 5 | public RecaptchaServiceException(String message, Throwable cause) { 6 | super(message, cause); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/service/user/UserService.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.service.user; 2 | 3 | import eu.kielczewski.example.domain.User; 4 | 5 | import java.util.List; 6 | 7 | public interface UserService { 8 | 9 | User save(User user); 10 | 11 | List getList(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/service/user/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.service.user; 2 | 3 | import eu.kielczewski.example.domain.User; 4 | import eu.kielczewski.example.repository.UserRepository; 5 | import eu.kielczewski.example.service.user.exception.UserAlreadyExistsException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import javax.validation.constraints.NotNull; 13 | import java.util.List; 14 | 15 | @Service 16 | public class UserServiceImpl implements UserService { 17 | 18 | private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class); 19 | private final UserRepository repository; 20 | 21 | @Autowired 22 | public UserServiceImpl(final UserRepository repository) { 23 | this.repository = repository; 24 | } 25 | 26 | @Override 27 | @Transactional 28 | public User save(@NotNull final User user) { 29 | LOGGER.debug("Creating {}", user); 30 | User existing = repository.findOne(user.getId()); 31 | if (existing != null) { 32 | throw new UserAlreadyExistsException( 33 | String.format("There already exists a user with id=%s", user.getId())); 34 | } 35 | return repository.save(user); 36 | } 37 | 38 | @Override 39 | @Transactional(readOnly = true) 40 | public List getList() { 41 | LOGGER.debug("Retrieving the list of all users"); 42 | return repository.findAll(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/service/user/exception/UserAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.service.user.exception; 2 | 3 | public class UserAlreadyExistsException extends RuntimeException { 4 | 5 | public UserAlreadyExistsException(final String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/validator/RecaptchaFormValidator.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.validator; 2 | 3 | import eu.kielczewski.example.domain.form.RecaptchaForm; 4 | import eu.kielczewski.example.service.recaptcha.RecaptchaService; 5 | import eu.kielczewski.example.service.recaptcha.exception.RecaptchaServiceException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.annotation.Scope; 10 | import org.springframework.context.annotation.ScopedProxyMode; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.validation.Errors; 13 | import org.springframework.validation.Validator; 14 | import org.springframework.web.client.RestClientException; 15 | import org.springframework.web.context.WebApplicationContext; 16 | 17 | import javax.servlet.http.HttpServletRequest; 18 | 19 | @Component 20 | @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) 21 | public class RecaptchaFormValidator implements Validator { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(RecaptchaFormValidator.class); 24 | private static final String ERROR_RECAPTCHA_INVALID = "recaptcha.error.invalid"; 25 | private static final String ERROR_RECAPTCHA_UNAVAILABLE = "recaptcha.error.unavailable"; 26 | private final HttpServletRequest httpServletRequest; 27 | private final RecaptchaService recaptchaService; 28 | 29 | @Autowired 30 | public RecaptchaFormValidator(HttpServletRequest httpServletRequest, RecaptchaService recaptchaService) { 31 | this.httpServletRequest = httpServletRequest; 32 | this.recaptchaService = recaptchaService; 33 | } 34 | 35 | @Override 36 | public boolean supports(Class> clazz) { 37 | return RecaptchaForm.class.isAssignableFrom(clazz); 38 | } 39 | 40 | @Override 41 | public void validate(Object target, Errors errors) { 42 | RecaptchaForm form = (RecaptchaForm) target; 43 | try { 44 | if (form.getRecaptchaResponse() != null 45 | && !form.getRecaptchaResponse().isEmpty() 46 | && !recaptchaService.isResponseValid(getRemoteIp(httpServletRequest), form.getRecaptchaResponse())) { 47 | errors.reject(ERROR_RECAPTCHA_INVALID); 48 | errors.rejectValue("recaptchaResponse", ERROR_RECAPTCHA_INVALID); 49 | } 50 | } catch (RecaptchaServiceException e) { 51 | LOGGER.error("Exception occurred when validating captcha response", e); 52 | errors.reject(ERROR_RECAPTCHA_UNAVAILABLE); 53 | } 54 | } 55 | 56 | private String getRemoteIp(HttpServletRequest request) { 57 | String ip = request.getHeader("x-forwarded-for"); 58 | if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 59 | ip = request.getHeader("Proxy-Client-IP"); 60 | } 61 | if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 62 | ip = request.getHeader("WL-Proxy-Client-IP"); 63 | } 64 | if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 65 | ip = request.getRemoteAddr(); 66 | } 67 | return ip; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/eu/kielczewski/example/validator/UserCreateFormPasswordValidator.java: -------------------------------------------------------------------------------- 1 | package eu.kielczewski.example.validator; 2 | 3 | import eu.kielczewski.example.domain.form.UserCreateForm; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.validation.Errors; 6 | import org.springframework.validation.Validator; 7 | 8 | @Component 9 | public class UserCreateFormPasswordValidator implements Validator { 10 | 11 | @Override 12 | public boolean supports(Class> clazz) { 13 | return UserCreateForm.class.isAssignableFrom(clazz); 14 | } 15 | 16 | @Override 17 | public void validate(Object target, Errors errors) { 18 | UserCreateForm form = (UserCreateForm) target; 19 | if (!form.getPassword1().equals(form.getPassword2())) { 20 | errors.rejectValue("password2", "user.error.password.no_match"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | 3 | # Spring 4 | spring.profiles.active=dev 5 | 6 | # Server 7 | server.port=8080 8 | server.sessionTimeout=30 9 | 10 | # Logger 11 | logging.level.eu.kielczewski=DEBUG 12 | logging.level.org.springframework.web=WARN 13 | logging.level=WARN 14 | 15 | # MVC 16 | spring.view.prefix=/WEB-INF/jsp/ 17 | spring.view.suffix=.jsp 18 | 19 | # JPA 20 | spring.jpa.hibernate.ddl-auto=create-drop 21 | 22 | # Tomcat 23 | tomcat.accessLogEnabled=false 24 | tomcat.protocolHeader=x-forwarded-proto 25 | tomcat.remoteIpHeader=x-forwarded-for 26 | tomcat.backgroundProcessorDelay=30 27 | 28 | # Recaptcha 29 | recaptcha.url=https://www.google.com/recaptcha/api/siteverify 30 | recaptcha.site-key=6LdRvN0SAAAAALwntZXun1_IjMyx_-LE3Zm7pJ8D 31 | recaptcha.secret-key=6LdRvN0SAAAAAOk4nA8ol3NKPeg-K_sNDfDikElt -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO user (id, password) VALUES ('user1', 'pass1'); 2 | INSERT INTO user (id, password) VALUES ('user2', 'pass1'); 3 | INSERT INTO user (id, password) VALUES ('user3', 'pass1'); 4 | INSERT INTO user (id, password) VALUES ('user4', 'pass1'); -------------------------------------------------------------------------------- /src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | 3 | user.list=List of users 4 | user.create=Create new user 5 | 6 | user.id=ID 7 | user.password1=Password 8 | user.password2=Repeat 9 | user.error.id.exists=User already exists 10 | user.error.password.no_match=Repeated password must be the same 11 | 12 | save=Save 13 | 14 | recaptcha.error.unavailable=Captcha service is unavailable at this time, try again later. 15 | recaptcha.error.invalid=The response to captcha was identified as invalid. Try again. 16 | 17 | 18 | NotEmpty=Required 19 | Size.form.id=ID length must be between {2} and {1} characters 20 | Size.form.password1=Password must be between {2} and {1} characters 21 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/user_create.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> 2 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> 3 | <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> 4 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags" %> 5 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 51 | 52 | --> 53 | 54 | 55 | 56 | 57 | 58 | "/> 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/user_list.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> 2 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 4 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags" %> 5 | <%--@elvariable id="users" type="java.util.List"--%> 6 | 7 | 8 | 9 | 10 | 11 | ID 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/layout.tag: -------------------------------------------------------------------------------- 1 | <%@ tag description="Layout Template" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 3 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> 4 | <%@ attribute name="tab" required="false" type="java.lang.String" %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | "/> 12 | 13 | 14 | 15 | 16 | class="active"> 17 | "> 18 | 19 | class="active"> 20 | "> 21 | 22 | 23 | 24 | 25 | 26 | 27 | "> 28 | 29 | --------------------------------------------------------------------------------