From 41b068b6d48f9d82babd1cfce3520ad0b28be861 Mon Sep 17 00:00:00 2001 From: Kamal Wickramanayake Date: Sat, 20 Jun 2026 18:25:56 +0530 Subject: Added Spring Boot role based security sample application --- spring-boot/10-role-based-security/.gitignore | 2 + spring-boot/10-role-based-security/README | 20 +++ spring-boot/10-role-based-security/pom.xml | 68 ++++++++++ .../main/java/com/example/spring/Application.java | 12 ++ .../java/com/example/spring/HomeController.java | 13 ++ .../com/example/spring/config/SecurityConfig.java | 36 +++++ .../contact/controller/ContactController.java | 49 +++++++ .../spring/contact/controller/ContactForm.java | 45 +++++++ .../com/example/spring/security/entity/User.java | 147 +++++++++++++++++++++ .../spring/security/repository/UserRepository.java | 13 ++ .../security/service/CustomUserDetailsService.java | 25 ++++ .../src/main/resources/application.properties | 1 + .../resources/config/application-dev.properties | 33 +++++ .../src/main/resources/data.sql | 5 + .../src/main/resources/static/css/main.css | 92 +++++++++++++ .../src/main/resources/templates/contact/form.html | 30 +++++ .../main/resources/templates/contact/result.html | 14 ++ .../src/main/resources/templates/index.html | 18 +++ .../src/main/resources/templates/layout/main.html | 36 +++++ 19 files changed, 659 insertions(+) create mode 100644 spring-boot/10-role-based-security/.gitignore create mode 100644 spring-boot/10-role-based-security/README create mode 100644 spring-boot/10-role-based-security/pom.xml create mode 100644 spring-boot/10-role-based-security/src/main/java/com/example/spring/Application.java create mode 100644 spring-boot/10-role-based-security/src/main/java/com/example/spring/HomeController.java create mode 100644 spring-boot/10-role-based-security/src/main/java/com/example/spring/config/SecurityConfig.java create mode 100644 spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactController.java create mode 100644 spring-boot/10-role-based-security/src/main/java/com/example/spring/contact/controller/ContactForm.java create mode 100644 spring-boot/10-role-based-security/src/main/java/com/example/spring/security/entity/User.java create mode 100644 spring-boot/10-role-based-security/src/main/java/com/example/spring/security/repository/UserRepository.java create mode 100644 spring-boot/10-role-based-security/src/main/java/com/example/spring/security/service/CustomUserDetailsService.java create mode 100644 spring-boot/10-role-based-security/src/main/resources/application.properties create mode 100644 spring-boot/10-role-based-security/src/main/resources/config/application-dev.properties create mode 100644 spring-boot/10-role-based-security/src/main/resources/data.sql create mode 100644 spring-boot/10-role-based-security/src/main/resources/static/css/main.css create mode 100644 spring-boot/10-role-based-security/src/main/resources/templates/contact/form.html create mode 100644 spring-boot/10-role-based-security/src/main/resources/templates/contact/result.html create mode 100644 spring-boot/10-role-based-security/src/main/resources/templates/index.html create mode 100644 spring-boot/10-role-based-security/src/main/resources/templates/layout/main.html (limited to 'spring-boot/10-role-based-security') diff --git a/spring-boot/10-role-based-security/.gitignore b/spring-boot/10-role-based-security/.gitignore new file mode 100644 index 0000000..3df278e --- /dev/null +++ b/spring-boot/10-role-based-security/.gitignore @@ -0,0 +1,2 @@ +target +.vscode diff --git a/spring-boot/10-role-based-security/README b/spring-boot/10-role-based-security/README new file mode 100644 index 0000000..9a79809 --- /dev/null +++ b/spring-boot/10-role-based-security/README @@ -0,0 +1,20 @@ +This sample project shows how to use role base security in a Spring Boot application. + +To run with support for hot swap during development, use the dev profile as follows: + + mvn spring-boot:run -Dspring-boot.run.profiles=dev + +User/Pasword (as defined in resources/data.sql) + + admin: abc123 + user1: abc123 + +How is role based security enabled? + +1. entity/User.java has been updated to return a list of SimpleGrantAuthority objects in the getAuthorities() method. + +2. config/SecurityConfig.java has been updated to enable web security and method security. + +3. Controllers and services can be annotated with @PreAuthorize(). Example: ContractController.java + +4. In Thymeleaf templates, sec:authorize() works with role names. Example: layout/main.html \ No newline at end of file diff --git a/spring-boot/10-role-based-security/pom.xml b/spring-boot/10-role-based-security/pom.xml new file mode 100644 index 0000000..96e4a92 --- /dev/null +++ b/spring-boot/10-role-based-security/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + com.example.spring.boot + base-config + 0.0.1-SNAPSHOT + ../00-config + + + thymeleaf-common-theme + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.postgresql + postgresql + 42.7.11 + + + + + org.bouncycastle + bcprov-jdk18on + 1.84 + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + \ No newline at end of file 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 roles = new HashSet<>(); // e.g., ["ADMIN", "USER"] + + @Override + public Collection 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 getRoles() { + return roles; + } + + public void setRoles(Set 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 { + Optional 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 diff --git a/spring-boot/10-role-based-security/src/main/resources/application.properties b/spring-boot/10-role-based-security/src/main/resources/application.properties new file mode 100644 index 0000000..8a8c4c2 --- /dev/null +++ b/spring-boot/10-role-based-security/src/main/resources/application.properties @@ -0,0 +1 @@ +# Look at resources/config/application-dev.properties for settings for dev profile. \ No newline at end of file diff --git a/spring-boot/10-role-based-security/src/main/resources/config/application-dev.properties b/spring-boot/10-role-based-security/src/main/resources/config/application-dev.properties new file mode 100644 index 0000000..50973c7 --- /dev/null +++ b/spring-boot/10-role-based-security/src/main/resources/config/application-dev.properties @@ -0,0 +1,33 @@ +# This file contains settings used during development. +# If using Maven to run, try the following command: +# mvn spring-boot:run -Dspring-boot.run.profiles=dev + +# Web related settings + +# Disable caching for static resources +spring.web.resources.cache.period=0 +spring.thymeleaf.cache=false + +# Optional: Tell DevTools to watch changes but never restart the backend server for CSS +spring.devtools.restart.exclude=static/**,public/**,templates/** + + +# Database related settings + +spring.datasource.url=jdbc:postgresql://localhost:5432/springboot +spring.datasource.username=springboot +spring.datasource.password=abc123 + +# Create/update/drop database tables automatically? +# In production, setting value to none is better. +# Possible values: none, validate, update, create, create-drop +spring.jpa.hibernate.ddl-auto=create-drop + +# Print sql that gets executed. Good for developers. Set to false in production. +spring.jpa.show-sql=true + +# Always execute initialization scripts (resources/schema.sql, resources/data.sql) on startup +spring.sql.init.mode=always + +# Data source initialization scripts (schema.sql and data.sql) are executed after Hibernate/JPA auto-creates database tables. +spring.jpa.defer-datasource-initialization=true diff --git a/spring-boot/10-role-based-security/src/main/resources/data.sql b/spring-boot/10-role-based-security/src/main/resources/data.sql new file mode 100644 index 0000000..5318008 --- /dev/null +++ b/spring-boot/10-role-based-security/src/main/resources/data.sql @@ -0,0 +1,5 @@ +INSERT INTO user_account (username, password, description) VALUES ('admin','$argon2id$v=19$m=16,t=2,p=1$YnJERWhhY0RxV1hGMFNWVg$OHRvhGZx1x9KqCoVZOfUD2TkJc+HQB5SamgYejw+K2Y', 'Admin user'); +INSERT INTO user_account (username, password, description) VALUES ('user1','$argon2id$v=19$m=16,t=2,p=1$YnJERWhhY0RxV1hGMFNWVg$OHRvhGZx1x9KqCoVZOfUD2TkJc+HQB5SamgYejw+K2Y', 'Normal user 1'); + +INSERT INTO user_roles (user_id, role_name) VALUES (1, 'ADMIN'); +INSERT INTO user_roles (user_id, role_name) VALUES (2, 'USER'); \ No newline at end of file diff --git a/spring-boot/10-role-based-security/src/main/resources/static/css/main.css b/spring-boot/10-role-based-security/src/main/resources/static/css/main.css new file mode 100644 index 0000000..edd53ea --- /dev/null +++ b/spring-boot/10-role-based-security/src/main/resources/static/css/main.css @@ -0,0 +1,92 @@ +body { + font-size: 1.125em; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.6em; + color: rgb(32, 34, 36);; + background-color: #FFF; + padding: 10px 5px; +} + +a { + color: #1475AC; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +ul li { + padding-bottom: 0.5em; +} + +header { + text-align: left; + font-size: 1.2em; +} + +header h1 { + margin-top: 0; + margin-bottom: 20px; +} + +header a { + color: #000000; + text-decoration: none; +} + +.main-nav { + margin-top: 0; + padding: 0; +} + +.main-nav ul { + margin: 0; + padding : 0; + list-style-type: none; + background-color: #F6F8F8; + border: 1px solid #eee; +} + +.main-nav ul li { + margin-left: 0; + display: inline; + padding: 0 8px; + border-left: 1px solid #F9F9FF; + border-right: 1px solid #F9F9FF; +} + +.main-nav ul li:hover { + background-color: white; + border-left: 1px solid #addfff; + border-right: 1px solid #addfff; +} + +.main-nav ul li a { + color: #555; + font-weight: bold; +} + +.main-nav ul li ai:hover { + color: #000; +} + +footer { + margin-top: 20px; + font-size: 85%; + border-top: 1px solid #CCC; +} + +.form-field-container { + margin-top: 8px; + margin-bottom: 8px; +} + +.form-field-container label { + display: inline-block; + width: 5em; +} + +.form-field-container .validation-error { + color: red; +} \ No newline at end of file diff --git a/spring-boot/10-role-based-security/src/main/resources/templates/contact/form.html b/spring-boot/10-role-based-security/src/main/resources/templates/contact/form.html new file mode 100644 index 0000000..7debe1f --- /dev/null +++ b/spring-boot/10-role-based-security/src/main/resources/templates/contact/form.html @@ -0,0 +1,30 @@ + + + + +
+
+
+ + +
+
+ +
+ + +
+
+ +
+ + +
+
+ + +
+
+ + + \ No newline at end of file diff --git a/spring-boot/10-role-based-security/src/main/resources/templates/contact/result.html b/spring-boot/10-role-based-security/src/main/resources/templates/contact/result.html new file mode 100644 index 0000000..d1bda9a --- /dev/null +++ b/spring-boot/10-role-based-security/src/main/resources/templates/contact/result.html @@ -0,0 +1,14 @@ + + + + +
+

We received your message.

+ +

+ + Try again +

+ + + \ No newline at end of file diff --git a/spring-boot/10-role-based-security/src/main/resources/templates/index.html b/spring-boot/10-role-based-security/src/main/resources/templates/index.html new file mode 100644 index 0000000..dce7a4e --- /dev/null +++ b/spring-boot/10-role-based-security/src/main/resources/templates/index.html @@ -0,0 +1,18 @@ + + + + +
+

Hello, Username Placeholder!

+ +

Hello, [[${#authentication.name}]]!

+ +

This is the home page content.

+ +

Use the navigation links to explore other pages.

+
+ + + \ No newline at end of file diff --git a/spring-boot/10-role-based-security/src/main/resources/templates/layout/main.html b/spring-boot/10-role-based-security/src/main/resources/templates/layout/main.html new file mode 100644 index 0000000..df96fc5 --- /dev/null +++ b/spring-boot/10-role-based-security/src/main/resources/templates/layout/main.html @@ -0,0 +1,36 @@ + + + + + Base title not shown + + + + + +
+

ABC App

+
+ + + +

Page content area title

+ + + + + + + \ No newline at end of file -- cgit v1.2.3