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 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