- Добавил reCaptcha

- Добавил поддержку rememberMe и хранение сессий в БД.

https://www.youtube.com/watch?v=7cDpbAbhyjc&list=PLU2ftbIeotGpAYRP9Iv2KLIwK36-o_qYk&index=14
This commit is contained in:
Leonid
2025-02-19 17:57:54 +03:00
parent 4bf7fb22c6
commit d2979fa7d4
10 changed files with 118 additions and 28 deletions

View File

@@ -81,6 +81,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,7 +1,9 @@
package net.sytes.kashey.sweater.config; package net.sytes.kashey.sweater.config;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -12,6 +14,11 @@ public class MvcConfig implements WebMvcConfigurer {
@Value("${upload.path}") @Value("${upload.path}")
private String uploadPath; private String uploadPath;
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public void addViewControllers(ViewControllerRegistry registry) { public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login"); registry.addViewController("/login").setViewName("login");

View File

@@ -2,6 +2,7 @@ package net.sytes.kashey.sweater.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -30,7 +31,7 @@ public class WebSecurityConfig {
.loginPage("/login") .loginPage("/login")
.defaultSuccessUrl("/main", true) .defaultSuccessUrl("/main", true)
.permitAll() .permitAll()
) ).rememberMe(Customizer.withDefaults())
.logout(LogoutConfigurer::permitAll); .logout(LogoutConfigurer::permitAll);
return http.build(); return http.build();

View File

@@ -2,21 +2,33 @@ package net.sytes.kashey.sweater.controller;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import net.sytes.kashey.sweater.domain.User; import net.sytes.kashey.sweater.domain.User;
import net.sytes.kashey.sweater.domain.dto.CaptchaResponseDto;
import net.sytes.kashey.sweater.service.CustomUserDetailsService; import net.sytes.kashey.sweater.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.client.RestTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import java.util.Collections;
import java.util.Objects;
@Controller @Controller
public class RegistrationController { public class RegistrationController {
private final static String CAPTCHA_URL = "https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s";
private final CustomUserDetailsService customUserDetailsService; 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.customUserDetailsService = customUserDetailsService;
this.restTemplate = restTemplate;
} }
@GetMapping("/registration") @GetMapping("/registration")
@@ -26,13 +38,31 @@ public class RegistrationController {
} }
@PostMapping("/registration") @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!"); 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)); model.mergeAttributes(ControllerUtils.getErrors(bindingResult));
return "registration"; return "registration";
} }
@@ -50,8 +80,10 @@ public class RegistrationController {
public String activateAccount(Model model, @PathVariable String activateCode) { public String activateAccount(Model model, @PathVariable String activateCode) {
if (customUserDetailsService.activateUser(activateCode)) { if (customUserDetailsService.activateUser(activateCode)) {
model.addAttribute("messageType", "success");
model.addAttribute("message", "User successfully activated!"); model.addAttribute("message", "User successfully activated!");
} else { } else {
model.addAttribute("messageType", "danger");
model.addAttribute("message", "User activation is failed!"); model.addAttribute("message", "User activation is failed!");
} }

View File

@@ -4,11 +4,15 @@ import jakarta.persistence.*;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
import java.util.Set; import java.util.Set;
@Entity @Entity
@Table(name = "usr") @Table(name = "usr")
public class User { public class User implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@@ -18,9 +22,6 @@ public class User {
@NotBlank(message = "Password must not be empty.") @NotBlank(message = "Password must not be empty.")
private String password; private String password;
@Transient
@NotBlank(message = "Password confirmation must not be empty.")
private String password2;
private boolean enabled; private boolean enabled;
@Email(message = "E-mail is not valid.") @Email(message = "E-mail is not valid.")
@@ -100,12 +101,4 @@ public class User {
public void setActivationCode(String activationCode) { public void setActivationCode(String activationCode) {
this.activationCode = activationCode; this.activationCode = activationCode;
} }
public String getPassword2() {
return password2;
}
public void setPassword2(String password2) {
this.password2 = password2;
}
} }

View File

@@ -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<String> errorCodes;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public Set<String> getErrorCodes() {
return errorCodes;
}
public void setErrorCodes(Set<String> errorCodes) {
this.errorCodes = errorCodes;
}
}

View File

@@ -4,14 +4,22 @@ spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/sweater_db
spring.datasource.username=user spring.datasource.username=user
spring.datasource.password=123456 spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.freemarker.expose-request-attributes=true spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true spring.freemarker.expose-session-attributes=true
spring.freemarker.suffix=.ftl spring.freemarker.suffix=.ftl
upload.path=/home/kashey/soft/sweater/uploads upload.path=/home/kashey/soft/sweater/uploads
hosting.path=http://localhost:8080 hosting.path=http://localhost:8080
spring.mail.host=smtp.yandex.ru spring.mail.host=smtp.yandex.ru
spring.mail.username=username@yandex.ru spring.mail.username=kasheyne@yandex.ru
spring.mail.password=pa$$word spring.mail.password=xyyfvjuoiapkerqr
spring.mail.port=465 spring.mail.port=465
spring.mail.protocol=smtps spring.mail.protocol=smtps
mail.debug=true mail.debug=true
recaptcha.secret=6LcB49sqAAAAALtmKoXaCFXIr4MqgQyoKHwPiMQO
spring.session.jdbc.serialize=java
spring.session.jdbc.initialize-schema=always
spring.session.jdbc.table-name=SPRING_SESSION

View File

@@ -2,9 +2,14 @@
<#import "parts/login.ftl" as l> <#import "parts/login.ftl" as l>
<@com.page> <@com.page>
<#if Session?? && Session.SPRING_SECURITY_LAST_EXCEPTION??> <#if SPRING_SECURITY_LAST_EXCEPTION??>
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
${Session.SPRING_SECURITY_LAST_EXCEPTION.message} ${SPRING_SECURITY_LAST_EXCEPTION.message}
</div>
</#if>
<#if message??>
<div <#if messageType??>class="alert alert-${messageType}" role="alert"</#if> >
${message}
</div> </div>
</#if> </#if>
<@l.login "/login" false/> <@l.login "/login" false/>

View File

@@ -6,6 +6,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head> </head>
<body> <body>

View File

@@ -53,6 +53,14 @@
</#if> </#if>
</div> </div>
</div> </div>
<div class="col-sm-6">
<div class="g-recaptcha" data-sitekey="6LcB49sqAAAAAIvXz46_hEGqtYAzFYYGuY_Y9Rlg"></div>
<#if captchaError??>
<div class="alert alert-danger" role="alert">
${captchaError}
</div>
</#if>
</div>
</#if> </#if>
<#if !isRegistrationForm> <#if !isRegistrationForm>
<a href="/registration" class="btn btn-primary">Add new user</a> <a href="/registration" class="btn btn-primary">Add new user</a>