From fe4932fff563e4bdee35d0f4e4c3d417c9112600 Mon Sep 17 00:00:00 2001 From: Leonid Date: Wed, 19 Feb 2025 20:06:14 +0300 Subject: [PATCH] =?UTF-8?q?-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20reCaptcha=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D1=83=20rem?= =?UTF-8?q?emberMe=20=D0=B8=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B9=20=D0=B2=20=D0=91?= =?UTF-8?q?=D0=94.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://www.youtube.com/watch?v=7cDpbAbhyjc&list=PLU2ftbIeotGpAYRP9Iv2KLIwK36-o_qYk&index=14 --- pom.xml | 8 +++- .../kashey/sweater/config/MvcConfig.java | 7 +++ .../sweater/config/WebSecurityConfig.java | 3 +- .../controller/RegistrationController.java | 48 +++++++++++++++---- .../net/sytes/kashey/sweater/domain/User.java | 17 ++----- .../domain/dto/CaptchaResponseDto.java | 31 ++++++++++++ src/main/resources/application.properties | 9 +++- src/main/resources/templates/login.ftl | 9 +++- src/main/resources/templates/parts/common.ftl | 1 + src/main/resources/templates/parts/login.ftl | 8 ++++ 10 files changed, 115 insertions(+), 26 deletions(-) create mode 100644 src/main/java/net/sytes/kashey/sweater/domain/dto/CaptchaResponseDto.java diff --git a/pom.xml b/pom.xml index 08bc5f5..96bf85a 100644 --- a/pom.xml +++ b/pom.xml @@ -78,8 +78,12 @@ flyway-mysql - org.springframework.boot - spring-boot-starter-validation + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.session + spring-session-jdbc diff --git a/src/main/java/net/sytes/kashey/sweater/config/MvcConfig.java b/src/main/java/net/sytes/kashey/sweater/config/MvcConfig.java index 50049af..a0e68f3 100644 --- a/src/main/java/net/sytes/kashey/sweater/config/MvcConfig.java +++ b/src/main/java/net/sytes/kashey/sweater/config/MvcConfig.java @@ -1,7 +1,9 @@ package net.sytes.kashey.sweater.config; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -12,6 +14,11 @@ public class MvcConfig implements WebMvcConfigurer { @Value("${upload.path}") private String uploadPath; + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login"); diff --git a/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java b/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java index 01e2a36..c13c214 100644 --- a/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java +++ b/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java @@ -2,6 +2,7 @@ package net.sytes.kashey.sweater.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -30,7 +31,7 @@ public class WebSecurityConfig { .loginPage("/login") .defaultSuccessUrl("/main", true) .permitAll() - ) + ).rememberMe(Customizer.withDefaults()) .logout(LogoutConfigurer::permitAll); return http.build(); diff --git a/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java b/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java index a7d6424..8f27aca 100644 --- a/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java +++ b/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java @@ -2,21 +2,33 @@ package net.sytes.kashey.sweater.controller; import jakarta.validation.Valid; import net.sytes.kashey.sweater.domain.User; +import net.sytes.kashey.sweater.domain.dto.CaptchaResponseDto; import net.sytes.kashey.sweater.service.CustomUserDetailsService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.util.ObjectUtils; 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.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.Objects; @Controller public class RegistrationController { + + private final static String CAPTCHA_URL = "https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s"; private final CustomUserDetailsService customUserDetailsService; - public RegistrationController(CustomUserDetailsService customUserDetailsService) { + @Value("${recaptcha.secret}") + private String recaptchaSecret; + + private final RestTemplate restTemplate; + + public RegistrationController(CustomUserDetailsService customUserDetailsService, RestTemplate restTemplate) { this.customUserDetailsService = customUserDetailsService; + this.restTemplate = restTemplate; } @GetMapping("/registration") @@ -26,13 +38,31 @@ public class RegistrationController { } @PostMapping("/registration") - public String addUser(@ModelAttribute @Valid User user, BindingResult bindingResult, Model model) { + public String addUser(@RequestParam(name = "g-recaptcha-response") String captchaResponse, + @RequestParam(name = "password2") String passwordConfirmation, + @ModelAttribute @Valid User user, + BindingResult bindingResult, + Model model) { + String url = String.format(CAPTCHA_URL, recaptchaSecret, captchaResponse); + CaptchaResponseDto responseDto = restTemplate.postForObject(url, Collections.EMPTY_LIST, CaptchaResponseDto.class); - if (user.getPassword() != null && !user.getPassword().equals(user.getPassword2())) { + boolean isCaptchaEmpty = !Objects.requireNonNull(responseDto).isSuccess(); + + if (isCaptchaEmpty) { + model.addAttribute("captchaError", "Fill the captcha"); + } + + if (user.getPassword() != null && !user.getPassword().equals(passwordConfirmation)) { model.addAttribute("passwordError", "Passwords do not match!"); } - if (bindingResult.hasErrors()) { + boolean isConfirmationEmpty = ObjectUtils.isEmpty(passwordConfirmation); + + if (isConfirmationEmpty) { + model.addAttribute("password2Error", "Password confirmation must not be empty."); + } + + if (bindingResult.hasErrors() || isConfirmationEmpty || isCaptchaEmpty) { model.mergeAttributes(ControllerUtils.getErrors(bindingResult)); return "registration"; } @@ -50,8 +80,10 @@ public class RegistrationController { public String activateAccount(Model model, @PathVariable String activateCode) { if (customUserDetailsService.activateUser(activateCode)) { + model.addAttribute("messageType", "success"); model.addAttribute("message", "User successfully activated!"); } else { + model.addAttribute("messageType", "danger"); model.addAttribute("message", "User activation is failed!"); } diff --git a/src/main/java/net/sytes/kashey/sweater/domain/User.java b/src/main/java/net/sytes/kashey/sweater/domain/User.java index 8af54ff..3beaf47 100644 --- a/src/main/java/net/sytes/kashey/sweater/domain/User.java +++ b/src/main/java/net/sytes/kashey/sweater/domain/User.java @@ -4,11 +4,15 @@ import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import java.io.Serial; +import java.io.Serializable; import java.util.Set; @Entity @Table(name = "usr") -public class User { +public class User implements Serializable { + @Serial + private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @@ -18,9 +22,6 @@ public class User { @NotBlank(message = "Password must not be empty.") private String password; - @Transient - @NotBlank(message = "Password confirmation must not be empty.") - private String password2; private boolean enabled; @Email(message = "E-mail is not valid.") @@ -100,12 +101,4 @@ public class User { public void setActivationCode(String activationCode) { this.activationCode = activationCode; } - - public String getPassword2() { - return password2; - } - - public void setPassword2(String password2) { - this.password2 = password2; - } } diff --git a/src/main/java/net/sytes/kashey/sweater/domain/dto/CaptchaResponseDto.java b/src/main/java/net/sytes/kashey/sweater/domain/dto/CaptchaResponseDto.java new file mode 100644 index 0000000..81d5f26 --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/domain/dto/CaptchaResponseDto.java @@ -0,0 +1,31 @@ +package net.sytes.kashey.sweater.domain.dto; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Set; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CaptchaResponseDto { + + private boolean success; + + @JsonAlias("error-codes") + private Set errorCodes; + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public Set getErrorCodes() { + return errorCodes; + } + + public void setErrorCodes(Set errorCodes) { + this.errorCodes = errorCodes; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 35ede6f..f0eed7e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,14 +4,21 @@ spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/sweater_db spring.datasource.username=user spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + spring.freemarker.expose-request-attributes=true spring.freemarker.expose-session-attributes=true spring.freemarker.suffix=.ftl upload.path=/home/kashey/soft/sweater/uploads hosting.path=http://localhost:8080 + spring.mail.host=smtp.yandex.ru spring.mail.username=username@yandex.ru spring.mail.password=pa$$word spring.mail.port=465 spring.mail.protocol=smtps -mail.debug=true \ No newline at end of file +mail.debug=true + +recaptcha.secret= +spring.session.jdbc.serialize=java +spring.session.jdbc.initialize-schema=always +spring.session.jdbc.table-name=SPRING_SESSION diff --git a/src/main/resources/templates/login.ftl b/src/main/resources/templates/login.ftl index fca2ec4..393b687 100644 --- a/src/main/resources/templates/login.ftl +++ b/src/main/resources/templates/login.ftl @@ -2,9 +2,14 @@ <#import "parts/login.ftl" as l> <@com.page> - <#if Session?? && Session.SPRING_SECURITY_LAST_EXCEPTION??> + <#if SPRING_SECURITY_LAST_EXCEPTION??> + + <#if message??> +
class="alert alert-${messageType}" role="alert" > + ${message}
<@l.login "/login" false/> diff --git a/src/main/resources/templates/parts/common.ftl b/src/main/resources/templates/parts/common.ftl index 5db2558..4fb35ab 100644 --- a/src/main/resources/templates/parts/common.ftl +++ b/src/main/resources/templates/parts/common.ftl @@ -6,6 +6,7 @@ + diff --git a/src/main/resources/templates/parts/login.ftl b/src/main/resources/templates/parts/login.ftl index 22d5dec..20fdd5a 100644 --- a/src/main/resources/templates/parts/login.ftl +++ b/src/main/resources/templates/parts/login.ftl @@ -53,6 +53,14 @@ +
+
+ <#if captchaError??> + + +
<#if !isRegistrationForm> Add new user