From 71b2cfefaed96fecb808ac30b05d593ec343600f Mon Sep 17 00:00:00 2001
From: Pedro Lucas Garcia <190115548@aluno.unb.br>
Date: Thu, 15 Aug 2024 17:31:14 -0300
Subject: [PATCH] Cria a entidade Account e configura Spring Security
---
mvp/pom.xml | 4 +
.../com/mymarket/mvp/accounts/Account.java | 122 ++++++++++++++++++
.../mvp/accounts/AccountController.java | 5 +
.../mvp/accounts/AccountRepository.java | 11 ++
.../mymarket/mvp/accounts/AccountService.java | 5 +
.../java/com/mymarket/mvp/accounts/Role.java | 16 +++
.../com/mymarket/mvp/accounts/UserDetail.java | 69 ++++++++++
.../mvp/accounts/UserDetailService.java | 21 +++
.../mymarket/mvp/shared/config/MvcConfig.java | 32 +++++
.../mvp/shared/config/SecurityConfig.java | 118 +++++++++++++++++
mvp/src/main/resources/application.properties | 4 +-
11 files changed, 404 insertions(+), 3 deletions(-)
create mode 100644 mvp/src/main/java/com/mymarket/mvp/accounts/Account.java
create mode 100644 mvp/src/main/java/com/mymarket/mvp/accounts/AccountController.java
create mode 100644 mvp/src/main/java/com/mymarket/mvp/accounts/AccountRepository.java
create mode 100644 mvp/src/main/java/com/mymarket/mvp/accounts/AccountService.java
create mode 100644 mvp/src/main/java/com/mymarket/mvp/accounts/Role.java
create mode 100644 mvp/src/main/java/com/mymarket/mvp/accounts/UserDetail.java
create mode 100644 mvp/src/main/java/com/mymarket/mvp/accounts/UserDetailService.java
create mode 100644 mvp/src/main/java/com/mymarket/mvp/shared/config/MvcConfig.java
create mode 100644 mvp/src/main/java/com/mymarket/mvp/shared/config/SecurityConfig.java
diff --git a/mvp/pom.xml b/mvp/pom.xml
index 1049e58..872ee46 100644
--- a/mvp/pom.xml
+++ b/mvp/pom.xml
@@ -42,6 +42,10 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
org.springframework.boot
spring-boot-starter-web
diff --git a/mvp/src/main/java/com/mymarket/mvp/accounts/Account.java b/mvp/src/main/java/com/mymarket/mvp/accounts/Account.java
new file mode 100644
index 0000000..6ae1b3e
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/accounts/Account.java
@@ -0,0 +1,122 @@
+package com.mymarket.mvp.accounts;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+import org.hibernate.validator.constraints.Length;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * The Account entity represents user account in the application.
+ *
+ * @author Pedro Lucas
+ * @since 1.0
+ */
+@Entity
+@Table(name = "accounts", uniqueConstraints = {@UniqueConstraint(columnNames = {"username"})})
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode(of = "id")
+@ToString(of = {"id", "firstName"})
+public final class Account implements Serializable {
+
+ /**
+ * The serialVersionUID.
+ */
+ @Serial
+ private static final long serialVersionUID = 221625420706334299L;
+
+ /**
+ * The unique identifier for the account.
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ /**
+ * The username for authentication. Must contain only letters and numbers.
+ */
+ @Column(length = 23, nullable = false, unique = true)
+ @Length(min = 6, max = 23)
+ @NotBlank(message = "The username cannot be blank")
+ @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]+$", message = "The username must contain only letters (at least one letter) and numbers (at least one number)")
+ private String username;
+
+ /**
+ * The password for authentication. Must contain at least one letter and one
+ * number.
+ */
+ @Column(name = "password", nullable = false)
+ @JsonIgnore
+ @NotBlank(message = "The password cannot be blank")
+ private String password;
+
+ /**
+ * The first name of the account holder.
+ */
+ @Column(length = 30, nullable = false)
+ @Length(min = 1, max = 30)
+ @NotBlank(message = "The name cannot be blank")
+ private String firstName;
+
+ /**
+ * (Optional) The last name or surname of the account holder.
+ */
+ @Column(length = 30)
+ @Length(max = 30)
+ private String lastName;
+
+ /**
+ * Indicates whether it is account non expired. False by default.
+ */
+ @Column(columnDefinition = "boolean default false", nullable = false)
+ private boolean isAccountNonExpired;
+
+ /**
+ * Indicates whether it is account non-locked. False by default.
+ */
+ @Column(columnDefinition = "boolean default false", nullable = false)
+ private boolean isAccountNonLocked;
+
+ /**
+ * Indicates whether it is account with credentials non expired. False by
+ * default.
+ */
+ @Column(columnDefinition = "boolean default false", nullable = false)
+ private boolean isCredentialsNonExpired;
+
+ /**
+ * Indicates whether it is enabled. False by default.
+ */
+ @Column(columnDefinition = "boolean default false", nullable = false)
+ private boolean isEnabled;
+
+ /**
+ * The role of the account in the system.
+ */
+ @Column(name = "role", nullable = false)
+ @Enumerated(EnumType.STRING)
+ private Role role;
+
+}
\ No newline at end of file
diff --git a/mvp/src/main/java/com/mymarket/mvp/accounts/AccountController.java b/mvp/src/main/java/com/mymarket/mvp/accounts/AccountController.java
new file mode 100644
index 0000000..66e77b5
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/accounts/AccountController.java
@@ -0,0 +1,5 @@
+package com.mymarket.mvp.accounts;
+
+public class AccountController {
+
+}
diff --git a/mvp/src/main/java/com/mymarket/mvp/accounts/AccountRepository.java b/mvp/src/main/java/com/mymarket/mvp/accounts/AccountRepository.java
new file mode 100644
index 0000000..3f4b9db
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/accounts/AccountRepository.java
@@ -0,0 +1,11 @@
+package com.mymarket.mvp.accounts;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface AccountRepository extends JpaRepository{
+ Optional findByUsername(String username);
+}
diff --git a/mvp/src/main/java/com/mymarket/mvp/accounts/AccountService.java b/mvp/src/main/java/com/mymarket/mvp/accounts/AccountService.java
new file mode 100644
index 0000000..3ee2b75
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/accounts/AccountService.java
@@ -0,0 +1,5 @@
+package com.mymarket.mvp.accounts;
+
+public class AccountService {
+
+}
diff --git a/mvp/src/main/java/com/mymarket/mvp/accounts/Role.java b/mvp/src/main/java/com/mymarket/mvp/accounts/Role.java
new file mode 100644
index 0000000..62c0a6f
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/accounts/Role.java
@@ -0,0 +1,16 @@
+package com.mymarket.mvp.accounts;
+
+public enum Role {
+ ROLE_ADMIN("admin"), ROLE_USER("user");
+
+ private final String value;
+
+ Role(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+}
\ No newline at end of file
diff --git a/mvp/src/main/java/com/mymarket/mvp/accounts/UserDetail.java b/mvp/src/main/java/com/mymarket/mvp/accounts/UserDetail.java
new file mode 100644
index 0000000..353da5b
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/accounts/UserDetail.java
@@ -0,0 +1,69 @@
+package com.mymarket.mvp.accounts;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.io.Serial;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * UserDetail implements the UserDetails interface for Spring Security
+ * integration.
+ *
+ * @since 1.0
+ * @author Pedro Lucas
+ */
+public class UserDetail implements UserDetails {
+
+ /** The serial version UID. */
+ @Serial
+ private static final long serialVersionUID = -6676620567638122748L;
+
+ private final Account account;
+
+ public UserDetail(Account account) {
+ this.account = account;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ if (this.account.getRole() == Role.ROLE_ADMIN) {
+ return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER"));
+ } else {
+ return List.of(new SimpleGrantedAuthority("ROLE_USER"));
+ }
+ }
+
+ @Override
+ public String getPassword() {
+ return this.account.getPassword();
+ }
+
+ @Override
+ public String getUsername() {
+ return this.account.getUsername();
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return this.account.isAccountNonExpired();
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return this.account.isAccountNonLocked();
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return this.account.isCredentialsNonExpired();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return this.account.isEnabled();
+ }
+
+}
\ No newline at end of file
diff --git a/mvp/src/main/java/com/mymarket/mvp/accounts/UserDetailService.java b/mvp/src/main/java/com/mymarket/mvp/accounts/UserDetailService.java
new file mode 100644
index 0000000..9169fcb
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/accounts/UserDetailService.java
@@ -0,0 +1,21 @@
+package com.mymarket.mvp.accounts;
+
+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 UserDetailService implements UserDetailsService {
+ private final AccountRepository accountRepository;
+
+ public UserDetailService(AccountRepository accountRepository) {
+ this.accountRepository = accountRepository;
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ return this.accountRepository.findByUsername(username).map(UserDetail::new)
+ .orElseThrow(() -> new UsernameNotFoundException("Username not found: " + username));
+ }
+}
\ No newline at end of file
diff --git a/mvp/src/main/java/com/mymarket/mvp/shared/config/MvcConfig.java b/mvp/src/main/java/com/mymarket/mvp/shared/config/MvcConfig.java
new file mode 100644
index 0000000..a5b1e81
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/shared/config/MvcConfig.java
@@ -0,0 +1,32 @@
+package com.mymarket.mvp.shared.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.lang.NonNull;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
+import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+
+@Configuration
+public class MvcConfig implements WebMvcConfigurer {
+
+ @Bean
+ LocaleResolver localeResolver() {
+ return new SessionLocaleResolver();
+ }
+
+ @Bean
+ LocaleChangeInterceptor localeChangeInterceptor() {
+ LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
+ localeChangeInterceptor.setParamName("lang");
+ return localeChangeInterceptor;
+ }
+
+ @Override
+ public void addInterceptors(@NonNull InterceptorRegistry interceptorRegistry) {
+ interceptorRegistry.addInterceptor(localeChangeInterceptor());
+ }
+
+}
\ No newline at end of file
diff --git a/mvp/src/main/java/com/mymarket/mvp/shared/config/SecurityConfig.java b/mvp/src/main/java/com/mymarket/mvp/shared/config/SecurityConfig.java
new file mode 100644
index 0000000..7e03f73
--- /dev/null
+++ b/mvp/src/main/java/com/mymarket/mvp/shared/config/SecurityConfig.java
@@ -0,0 +1,118 @@
+package com.mymarket.mvp.shared.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+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;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
+import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
+import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
+import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive;
+import org.springframework.security.web.session.HttpSessionEventPublisher;
+import org.springframework.session.FindByIndexNameSessionRepository;
+import org.springframework.session.Session;
+import org.springframework.session.security.SpringSessionBackedSessionRegistry;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.mymarket.mvp.accounts.UserDetailService;
+
+@Configuration
+@EnableMethodSecurity
+@EnableWebSecurity
+public class SecurityConfig {
+ private final FindByIndexNameSessionRepository sessionRepository;
+ private final UserDetailService userDetailService;
+
+ public SecurityConfig(FindByIndexNameSessionRepository sessionRepository, UserDetailService userDetailService) {
+ this.sessionRepository = sessionRepository;
+ this.userDetailService = userDetailService;
+ }
+
+ @Bean
+ SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
+
+ HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(
+ new ClearSiteDataHeaderWriter(Directive.ALL));
+
+ httpSecurity.cors(Customizer.withDefaults()).csrf(csrf -> csrf.
+ ignoringRequestMatchers("/h2-console/**").disable())
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers("/h2-console/**").permitAll()
+ .requestMatchers("/api/public/**").permitAll()
+ .requestMatchers("/api/auth/**").hasAnyRole("ADMIN", "USER")
+ .requestMatchers("/api/private/**").hasRole("ADMIN")
+ .anyRequest().authenticated())
+ .userDetailsService(userDetailService)
+ .sessionManagement(session -> session
+ .sessionFixation().migrateSession()
+ .invalidSessionUrl("/api/public/authentication/login?invalid")
+ .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).maximumSessions(2)
+ .maxSessionsPreventsLogin(false).expiredUrl("/api/public/authentication/login?expired")
+ .sessionRegistry(sessionRegistry()))
+ .logout(logout -> logout
+ .addLogoutHandler(clearSiteData)
+ .deleteCookies("SESSION")
+ .invalidateHttpSession(true)
+ .logoutUrl("/api/auth/authentication/logout")
+ .logoutSuccessUrl("/api/public/authentication/login?logout")
+ .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)))
+ .httpBasic(Customizer.withDefaults())
+ .headers(httpSecurityHeadersConfigurer -> {
+ httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable);
+ });
+
+ return httpSecurity.build();
+ }
+
+ @Bean
+ CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ configuration.setAllowedOrigins(List.of("http://localhost:4200"));
+ configuration.setAllowedMethods(
+ Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE", "CONNECT"));
+ configuration.setAllowedHeaders(List.of("Accept", "Cookie", "Set-Cookie", "Origin", "Content-Type", "Depth",
+ "User-Agent", "If-Modified-Since", "Cache-Control", "Authorization", "X-Req", "X-File-Size",
+ "X-Requested-With", "X-File-Name"));
+ configuration.setExposedHeaders(List.of("Cookie", "Set-Cookie", "Authorization"));
+ configuration.setAllowCredentials(true);
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+
+ @Bean
+ SpringSessionBackedSessionRegistry sessionRegistry() {
+ return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
+ }
+
+ @Bean
+ HttpSessionEventPublisher httpSessionEventPublisher() {
+ return new HttpSessionEventPublisher();
+ }
+
+ @Bean
+ AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
+ throws Exception {
+ return authenticationConfiguration.getAuthenticationManager();
+ }
+
+ @Bean
+ PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
\ No newline at end of file
diff --git a/mvp/src/main/resources/application.properties b/mvp/src/main/resources/application.properties
index 4ed4e36..77745b4 100644
--- a/mvp/src/main/resources/application.properties
+++ b/mvp/src/main/resources/application.properties
@@ -19,8 +19,6 @@ server.error.include-stacktrace=never
server.error.include-message=never
server.ssl.enabled = false
-spring.application.name=dev
-
# H2 cache database configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
@@ -34,4 +32,4 @@ spring.jpa.defer-datasource-initialization=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.trace=false
-spring.h2.console.settings.web-allow-others=false
\ No newline at end of file
+spring.h2.console.settings.web-allow-others=true
\ No newline at end of file