summaryrefslogtreecommitdiff
path: root/spring-boot/10-role-based-security/src/main/java/com/example
diff options
context:
space:
mode:
Diffstat (limited to 'spring-boot/10-role-based-security/src/main/java/com/example')
-rw-r--r--spring-boot/10-role-based-security/src/main/java/com/example/spring/Application.java12
-rw-r--r--spring-boot/10-role-based-security/src/main/java/com/example/spring/HomeController.java13
-rw-r--r--spring-boot/10-role-based-security/src/main/java/com/example/spring/config/SecurityConfig.java36
-rw-r--r--spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactController.java49
-rw-r--r--spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactForm.java45
-rw-r--r--spring-boot/10-role-based-security/src/main/java/com/example/spring/security/entity/User.java147
-rw-r--r--spring-boot/10-role-based-security/src/main/java/com/example/spring/security/repository/UserRepository.java13
-rw-r--r--spring-boot/10-role-based-security/src/main/java/com/example/spring/security/service/CustomUserDetailsService.java25
8 files changed, 340 insertions, 0 deletions
diff --git a/spring-boot/10-role-based-security/src/main/java/com/example/spring/Application.java b/spring-boot/10-role-based-security/src/main/java/com/example/spring/Application.java
new file mode 100644
index 0000000..4393f3f
--- /dev/null
+++ b/spring-boot/10-role-based-security/src/main/java/com/example/spring/Application.java
@@ -0,0 +1,12 @@
+package com.example.spring;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) throws Exception {
+ SpringApplication.run(Application.class, args);
+ }
+} \ No newline at end of file
diff --git a/spring-boot/10-role-based-security/src/main/java/com/example/spring/HomeController.java b/spring-boot/10-role-based-security/src/main/java/com/example/spring/HomeController.java
new file mode 100644
index 0000000..d6ead56
--- /dev/null
+++ b/spring-boot/10-role-based-security/src/main/java/com/example/spring/HomeController.java
@@ -0,0 +1,13 @@
+package com.example.spring;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+public class HomeController {
+
+ @RequestMapping("/")
+ public String home() {
+ return "index";
+ }
+}
diff --git a/spring-boot/10-role-based-security/src/main/java/com/example/spring/config/SecurityConfig.java b/spring-boot/10-role-based-security/src/main/java/com/example/spring/config/SecurityConfig.java
new file mode 100644
index 0000000..8359405
--- /dev/null
+++ b/spring-boot/10-role-based-security/src/main/java/com/example/spring/config/SecurityConfig.java
@@ -0,0 +1,36 @@
+package com.example.spring.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+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.crypto.argon2.Argon2PasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+public class SecurityConfig {
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ // An Argon2 password encoder with a salt length of 16 bytes, a hash length of 32 bytes, parallelism of 1, memory cost of 1 << 14 (i.e. 16MB) and 2 iterations.
+ return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
+ }
+
+ // @Bean
+ // public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ // http
+ // //.csrf(csrf -> csrf.disable()) // Disable for stateless REST APIs
+ // .authorizeHttpRequests(auth -> auth
+ // .requestMatchers("/somepath/**").permitAll() // Publicly available
+ // .requestMatchers("/admin/**").hasRole("ADMIN") // Requires ROLE_ADMIN
+ // .requestMatchers("/contact/**").hasAnyRole("USER", "ADMIN")
+ // .anyRequest().authenticated()
+ // );
+
+ // return http.build();
+ // }
+} \ No newline at end of file
diff --git a/spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactController.java b/spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactController.java
new file mode 100644
index 0000000..abc7e83
--- /dev/null
+++ b/spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactController.java
@@ -0,0 +1,49 @@
+package com.example.spring.contact.controller;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+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.PostMapping;
+
+import jakarta.validation.Valid;
+
+@Controller
+public class ContactController {
+
+ /*
+ * Some values that can be used with @PreAuthorize():
+ *
+ * hasRole('ADMIN'): Checks if the user contains the authority ROLE_ADMIN.
+ *
+ * hasAnyRole('USER', 'ADMIN'): Passes if the user has at least one matching
+ * authority.
+ *
+ * isAuthenticated(): Verifies that the client is logged into the system
+ */
+
+ @GetMapping("/contact")
+ @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
+ public String showForm(Model model) {
+ model.addAttribute("contactForm", new ContactForm());
+ return "contact/form";
+ }
+
+ @PostMapping("/contact")
+ @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
+ public String processSubmission(@Valid @ModelAttribute ContactForm contactForm, BindingResult bindingResult,
+ Model model) {
+ if (bindingResult.hasErrors()) {
+ return "contact/form";
+ }
+
+ // Do whatever needed with the submitted data here.
+ // For example, invoke a method in a service/componet bean to send an email to
+ // contact person.
+
+ return "contact/result";
+ }
+
+}
diff --git a/spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactForm.java b/spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactForm.java
new file mode 100644
index 0000000..925999b
--- /dev/null
+++ b/spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactForm.java
@@ -0,0 +1,45 @@
+package com.example.spring.contact.controller;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+public class ContactForm {
+
+ @Pattern(regexp = "^[ a-zA-Z]+$", message = "Name should only include English letters")
+ @NotBlank(message = "Name is required")
+ private String name;
+
+ @Email(message = "Must be a valid email")
+ @NotBlank(message = "Email is required")
+ private String email;
+
+ @Size(min = 10, message = "Message must be at least 10 characters long")
+ private String message;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+}
diff --git a/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/entity/User.java b/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/entity/User.java
new file mode 100644
index 0000000..72c413a
--- /dev/null
+++ b/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/entity/User.java
@@ -0,0 +1,147 @@
+package com.example.spring.security.entity;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import jakarta.persistence.CollectionTable;
+import jakarta.persistence.Column;
+import jakarta.persistence.ElementCollection;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.Table;
+
+/*
+ @Table - Used to set the database table name explicitely.
+*/
+@Entity
+@Table(name = "user_account")
+public class User implements UserDetails {
+
+ /*
+ * GenerationType.IDENTITY - Auto generate value based on database's native auto
+ * increment feature
+ */
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true)
+ private String username;
+
+ private String password;
+
+ @Column(nullable = false)
+ private String description;
+
+ @CreationTimestamp
+ @Column(updatable = false, name = "created_at")
+ private Date createdAt;
+
+ @UpdateTimestamp
+ @Column(name = "updated_at")
+ private Date updatedAt;
+
+ /*
+ * @ElementCollection: Marks the field as a collection of basic types (Strings,
+ * Integers, etc.) or embeddable objects.
+ *
+ * @CollectionTable: Customizes the junction table details. If omitted, JPA
+ * generates a default table name combining the entity name and the field name
+ * (e.g., User_roles).
+ *
+ * @Column: Defines the column name for the String values inside the collection
+ * table.
+ *
+ * Fetch Type: By default, element collections use FetchType.LAZY. If you access
+ * the set outside of an active transaction or Hibernate session, it throws a
+ * LazyInitializationException. You can switch it to fetch = FetchType.EAGER to
+ * load the strings immediately alongside the main entity.
+ *
+ */
+ @ElementCollection(fetch = FetchType.EAGER)
+ @CollectionTable(name = "user_roles", // Name of the separate collection table
+ joinColumns = @JoinColumn(name = "user_id") // Foreign key linking back to this entity
+ )
+ @Column(name = "role_name") // Name of the column storing the actual String values
+ private Set<String> roles = new HashSet<>(); // e.g., ["ADMIN", "USER"]
+
+ @Override
+ public Collection<? extends GrantedAuthority> getAuthorities() {
+ return roles.stream()
+ .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Date getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Date createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public Date getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(Date updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ public Set<String> getRoles() {
+ return roles;
+ }
+
+ public void setRoles(Set<String> roles) {
+ this.roles = roles;
+ }
+
+} \ No newline at end of file
diff --git a/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/repository/UserRepository.java b/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/repository/UserRepository.java
new file mode 100644
index 0000000..f67039c
--- /dev/null
+++ b/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/repository/UserRepository.java
@@ -0,0 +1,13 @@
+package com.example.spring.security.repository;
+
+import java.util.Optional;
+
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.stereotype.Repository;
+
+import com.example.spring.security.entity.User;
+
+@Repository
+public interface UserRepository extends ListCrudRepository<User, Long> {
+ Optional<User> findByUsername(String username);
+}
diff --git a/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/service/CustomUserDetailsService.java b/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/service/CustomUserDetailsService.java
new file mode 100644
index 0000000..cdced4a
--- /dev/null
+++ b/spring-boot/10-role-based-security/src/main/java/com/example/spring/security/service/CustomUserDetailsService.java
@@ -0,0 +1,25 @@
+package com.example.spring.security.service;
+
+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;
+
+import com.example.spring.security.repository.UserRepository;
+
+@Service
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+ public CustomUserDetailsService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ return userRepository.findByUsername(username)
+ .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+ }
+
+} \ No newline at end of file