diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..a90eceb --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + mysql.8 + true + com.mysql.cj.jdbc.Driver + jdbc:mysql://localhost:3306/sweater_db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/compose.yml b/compose.yml index f1b675b..0d844f6 100644 --- a/compose.yml +++ b/compose.yml @@ -7,5 +7,5 @@ services: - 'MYSQL_ROOT_PASSWORD=verysecret' - 'MYSQL_USER=user' ports: - - '3306' + - '3306:3306' diff --git a/pom.xml b/pom.xml index 4635ced..c939ddb 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,15 @@ mysql-connector-j runtime + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-core + 6.3.1 + diff --git a/src/main/java/net/sytes/kashey/sweater/config/MvcConfig.java b/src/main/java/net/sytes/kashey/sweater/config/MvcConfig.java new file mode 100644 index 0000000..cb1eb8f --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/config/MvcConfig.java @@ -0,0 +1,13 @@ +package net.sytes.kashey.sweater.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class MvcConfig implements WebMvcConfigurer { + public void addViewControllers(ViewControllerRegistry registry) { + + registry.addViewController("/login").setViewName("login"); + } +} \ No newline at end of file diff --git a/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java b/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java new file mode 100644 index 0000000..00f9ffd --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/config/WebSecurityConfig.java @@ -0,0 +1,34 @@ +package net.sytes.kashey.sweater.config; + +import net.sytes.kashey.sweater.service.CustomUserDetailsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.configurers.LogoutConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +import javax.sql.DataSource; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/", "/registration").permitAll() + .anyRequest().authenticated() + ) + .formLogin((form) -> form + .loginPage("/login") + .defaultSuccessUrl("/main", true) + .permitAll() + ) + .logout(LogoutConfigurer::permitAll); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/net/sytes/kashey/sweater/GreetingController.java b/src/main/java/net/sytes/kashey/sweater/controller/MainController.java similarity index 76% rename from src/main/java/net/sytes/kashey/sweater/GreetingController.java rename to src/main/java/net/sytes/kashey/sweater/controller/MainController.java index 31e0589..8a095c9 100644 --- a/src/main/java/net/sytes/kashey/sweater/GreetingController.java +++ b/src/main/java/net/sytes/kashey/sweater/controller/MainController.java @@ -1,4 +1,4 @@ -package net.sytes.kashey.sweater; +package net.sytes.kashey.sweater.controller; import net.sytes.kashey.sweater.domain.Message; import net.sytes.kashey.sweater.repository.MessageRepository; @@ -10,29 +10,27 @@ import org.springframework.web.bind.annotation.RequestParam; import java.util.Map; @Controller -public class GreetingController { +public class MainController { private final MessageRepository messageRepository; - public GreetingController(MessageRepository messageRepository) { + public MainController(MessageRepository messageRepository) { this.messageRepository = messageRepository; } - @GetMapping("/greeting") - public String greeting(@RequestParam(name = "name", required = false, defaultValue = "World") String name, - Map model) { - model.put("name", name); + @GetMapping("/") + public String greeting() { return "greeting"; } - @GetMapping + @GetMapping("/main") public String findAllMessages(Map model) { model.put("messages", messageRepository.findAll()); return "main"; } - @PostMapping + @PostMapping("/main") public String addMessage(@RequestParam String text, @RequestParam String tag, Map model) { messageRepository.save(new Message(text, tag)); diff --git a/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java b/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java new file mode 100644 index 0000000..7c2e4fd --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/controller/RegistrationController.java @@ -0,0 +1,48 @@ +package net.sytes.kashey.sweater.controller; + +import net.sytes.kashey.sweater.domain.Role; +import net.sytes.kashey.sweater.domain.User; +import net.sytes.kashey.sweater.repository.UserRepository; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +import java.util.Collections; +import java.util.Map; + +@Controller +public class RegistrationController { + + private final UserRepository userRepository; + + public RegistrationController(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @GetMapping("/registration") + public String registration() { + + return "registration"; + } + + @PostMapping("/registration") + public String addUser(@ModelAttribute User user, Map model) { + + if (user.getUsername() == null || user.getUsername().isEmpty() || user.getUsername().isBlank()) { + model.put("message", "User name must not be empty"); + return "registration"; + } + + if (userRepository.findByUsername(user.getUsername()).isPresent()) { + model.put("message", "User already exists"); + return "registration"; + } + + user.setEnabled(true); + user.setRoles(Collections.singleton(Role.USER)); + user.setPassword("{noop}" + user.getPassword()); //пока не введено шифрование паролей + userRepository.save(user); + return "redirect:/login"; + } +} diff --git a/src/main/java/net/sytes/kashey/sweater/domain/Role.java b/src/main/java/net/sytes/kashey/sweater/domain/Role.java new file mode 100644 index 0000000..9d58522 --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/domain/Role.java @@ -0,0 +1,5 @@ +package net.sytes.kashey.sweater.domain; + +public enum Role { + ADMIN, USER; +} diff --git a/src/main/java/net/sytes/kashey/sweater/domain/User.java b/src/main/java/net/sytes/kashey/sweater/domain/User.java new file mode 100644 index 0000000..368517d --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/domain/User.java @@ -0,0 +1,73 @@ +package net.sytes.kashey.sweater.domain; + +import jakarta.persistence.*; + +import java.util.Set; + +@Entity +@Table(name = "usr") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + private String username; + private String password; + private boolean enabled; + @ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER) + @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id")) + @Enumerated(EnumType.STRING) + private Set roles; + + public User() { + } + + public User(String username) { + this.username = username; + } + + public User(String username, String password) { + this.username = username; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String name) { + this.username = name; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } +} diff --git a/src/main/java/net/sytes/kashey/sweater/repository/UserRepository.java b/src/main/java/net/sytes/kashey/sweater/repository/UserRepository.java new file mode 100644 index 0000000..44e2231 --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/repository/UserRepository.java @@ -0,0 +1,12 @@ +package net.sytes.kashey.sweater.repository; + +import net.sytes.kashey.sweater.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String name); +} diff --git a/src/main/java/net/sytes/kashey/sweater/security/CustomUserDetails.java b/src/main/java/net/sytes/kashey/sweater/security/CustomUserDetails.java new file mode 100644 index 0000000..52c59a5 --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/security/CustomUserDetails.java @@ -0,0 +1,55 @@ +package net.sytes.kashey.sweater.security; + +import net.sytes.kashey.sweater.domain.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.stream.Collectors; + +public class CustomUserDetails implements UserDetails { + + private final User user; + + public CustomUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + return user.getRoles().stream() + .map(role -> new SimpleGrantedAuthority(role.name())) + .collect(Collectors.toSet()); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return user.isEnabled(); + } +} \ No newline at end of file diff --git a/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java b/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java new file mode 100644 index 0000000..4fcf513 --- /dev/null +++ b/src/main/java/net/sytes/kashey/sweater/service/CustomUserDetailsService.java @@ -0,0 +1,28 @@ +package net.sytes.kashey.sweater.service; + +import net.sytes.kashey.sweater.domain.User; +import net.sytes.kashey.sweater.repository.UserRepository; +import net.sytes.kashey.sweater.security.CustomUserDetails; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { + + User user = userRepository.findByUsername(name) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + name)); + return new CustomUserDetails(user); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 650d561..b47762d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,4 +3,5 @@ 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.mustache.servlet.expose-request-attributes=true diff --git a/src/main/resources/templates/greeting.mustache b/src/main/resources/templates/greeting.mustache index 1d5b344..679bd63 100644 --- a/src/main/resources/templates/greeting.mustache +++ b/src/main/resources/templates/greeting.mustache @@ -5,6 +5,7 @@ -
Hello {{name}}!!!
+
Hello there
+Main page \ No newline at end of file diff --git a/src/main/resources/templates/login.mustache b/src/main/resources/templates/login.mustache new file mode 100644 index 0000000..2662d99 --- /dev/null +++ b/src/main/resources/templates/login.mustache @@ -0,0 +1,16 @@ + + + + Spring Security Example + + +Login page +
+
+
+
+ +
+Add new user + + \ No newline at end of file diff --git a/src/main/resources/templates/main.mustache b/src/main/resources/templates/main.mustache index a8cfd43..88fb4b0 100644 --- a/src/main/resources/templates/main.mustache +++ b/src/main/resources/templates/main.mustache @@ -1,19 +1,29 @@ +
-
- - - -
-
+ + + + +
-
-Tag to filer -
- - -
+
+ + + + +
+
+
+
+ Tag to filer +
+ + + +
Messages list
@@ -24,6 +34,5 @@ Tag to filer {{tag}} {{/messages}} - \ No newline at end of file diff --git a/src/main/resources/templates/registration.mustache b/src/main/resources/templates/registration.mustache new file mode 100644 index 0000000..3c2ecc4 --- /dev/null +++ b/src/main/resources/templates/registration.mustache @@ -0,0 +1,20 @@ + + + + Spring Security Example + + +Registration page +
+{{#message}} + {{message}} +{{/message}} +
+
+
+
+ +
+ + + \ No newline at end of file