From aa122113ade36f02dc8fdbebdf1232b5c4b8742c Mon Sep 17 00:00:00 2001 From: Kamal Wickramanayake Date: Fri, 3 Jul 2026 19:02:36 +0530 Subject: Microservice sample projects --- .../resourceserver/ResourceServerApplication.java | 13 +++ .../resourceserver/config/SecurityConfig.java | 32 ++++++ .../java/com/example/resourceserver/post/Post.java | 56 +++++++++++ .../resourceserver/post/PostRepository.java | 8 ++ .../resourceserver/post/PostRestController.java | 22 +++++ .../example/resourceserver/post/PostService.java | 18 ++++ .../resourceserver/security/RolesResponse.java | 7 ++ .../com/example/resourceserver/security/User.java | 110 +++++++++++++++++++++ .../resourceserver/security/UserRepository.java | 11 +++ .../security/UserRestController.java | 71 +++++++++++++ .../resourceserver/security/UserService.java | 47 +++++++++ .../src/main/resources/application.yaml | 40 ++++++++ .../03-resource-server/src/main/resources/data.sql | 8 ++ 13 files changed, 443 insertions(+) create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/ResourceServerApplication.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/config/SecurityConfig.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/post/Post.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRepository.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRestController.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostService.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/security/RolesResponse.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/security/User.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRepository.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRestController.java create mode 100644 microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserService.java create mode 100644 microservices/03-resource-server/src/main/resources/application.yaml create mode 100644 microservices/03-resource-server/src/main/resources/data.sql (limited to 'microservices/03-resource-server/src/main') diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/ResourceServerApplication.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/ResourceServerApplication.java new file mode 100644 index 0000000..0bad23e --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/ResourceServerApplication.java @@ -0,0 +1,13 @@ +package com.example.resourceserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(ResourceServerApplication.class, args); + } + +} diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/config/SecurityConfig.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/config/SecurityConfig.java new file mode 100644 index 0000000..7102e69 --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/config/SecurityConfig.java @@ -0,0 +1,32 @@ +package com.example.resourceserver.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +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.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + // 1. Specify the URL to allow without restrictions + .requestMatchers("/api/security/users/*/roles", "/public/**").permitAll() + // 2. Require authentication for all other requests + .anyRequest().authenticated() + ) + // 3. Configure the app as an OAuth2 Resource Server + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(Customizer.withDefaults()) + ); + + return http.build(); + } +} diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/Post.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/Post.java new file mode 100644 index 0000000..c66ce1b --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/Post.java @@ -0,0 +1,56 @@ +package com.example.resourceserver.post; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; + +@Entity +public class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + private String author; + + @Lob + private String description; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + +} diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRepository.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRepository.java new file mode 100644 index 0000000..778ea92 --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRepository.java @@ -0,0 +1,8 @@ +package com.example.resourceserver.post; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostRepository extends JpaRepository { +} \ No newline at end of file diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRestController.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRestController.java new file mode 100644 index 0000000..1a3ce86 --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRestController.java @@ -0,0 +1,22 @@ +package com.example.resourceserver.post; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/posts") +public class PostRestController { + private PostService postService; + + public PostRestController(PostService postService) { + this.postService = postService; + } + + @GetMapping("") + public List getAll() { + return postService.findAll(); + } +} diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostService.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostService.java new file mode 100644 index 0000000..06194ac --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostService.java @@ -0,0 +1,18 @@ +package com.example.resourceserver.post; + +import java.util.List; + +import org.springframework.stereotype.Service; + +@Service +public class PostService { + private PostRepository postRepository; + + public PostService(PostRepository postRepository) { + this.postRepository = postRepository; + } + + public List findAll() { + return postRepository.findAll(); + } +} diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/RolesResponse.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/RolesResponse.java new file mode 100644 index 0000000..9d93729 --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/RolesResponse.java @@ -0,0 +1,7 @@ +package com.example.resourceserver.security; + +import java.util.Set; + +public record RolesResponse(Set roles) { + +} diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/User.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/User.java new file mode 100644 index 0000000..8f84297 --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/User.java @@ -0,0 +1,110 @@ +package com.example.resourceserver.security; + +import java.time.LocalDateTime; +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 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 { + + /* + * 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; + + /* + * @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"] + + public String getPassword() { + return password; + } + + 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 Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } +} \ No newline at end of file diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRepository.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRepository.java new file mode 100644 index 0000000..9bf9422 --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRepository.java @@ -0,0 +1,11 @@ +package com.example.resourceserver.security; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} \ No newline at end of file diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRestController.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRestController.java new file mode 100644 index 0000000..0eb0d51 --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRestController.java @@ -0,0 +1,71 @@ +package com.example.resourceserver.security; + +import java.security.Principal; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/security/users") +public class UserRestController { + private UserService userService; + + public UserRestController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/{username}/roles") + public ResponseEntity getRoles(@PathVariable String username) { + Optional user = userService.findByUsername(username); + + if (user.isEmpty()) { + return ResponseEntity.ok(new RolesResponse(Set.of())); + } + + return ResponseEntity.ok(new RolesResponse(user.get().getRoles())); + } + + @GetMapping("/test") + public String test(Principal principal) { + return principal.toString(); + } + + @GetMapping("/test-username") + public String testName(Principal principal) { + return principal.getName(); + } + + // Get roles using the Authentication object + @GetMapping("/test-roles-auth") + public Collection getRoles(Authentication authentication) { + return authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); + } + + // Inspect raw JWT claims directly + @GetMapping("/test-roles-jwt") + public Map getClaims(@AuthenticationPrincipal Jwt jwt) { + return jwt.getClaims(); // Extract custom JSON fields containing roles + } + + /* Not very secure because unverifiable headers can be introduced by unwanted parties */ + @GetMapping("/test-roles-headers") + public String getRolesFromHeaders(@RequestHeader("X-User-Name") String username, + @RequestHeader("X-User-Roles") String roles) { + return "Roles: " + roles + ", username: " + username ; + } +} diff --git a/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserService.java b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserService.java new file mode 100644 index 0000000..1f6ddac --- /dev/null +++ b/microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserService.java @@ -0,0 +1,47 @@ +package com.example.resourceserver.security; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + public Optional findById(Long id) { + return userRepository.findById(id); + } + + public List findAll() { + return userRepository.findAll(Sort.by(Sort.Direction.ASC, "username")); + } + + @PreAuthorize("hasRole('ADMIN')") + @Transactional + public void updateUser(Long id, String description) { + // Do whatever the work needed. + // ... + + Optional userOpt = userRepository.findById(id); + + User user = userOpt.get(); + + user.setDescription(description); + + // NO userRepository.save(user) IS ACTUALLY REQUIRED HERE since the method + // has been annotated with @Transactional! + // Transaction commits -> Hibernate issues the SQL UPDATE. + } +} diff --git a/microservices/03-resource-server/src/main/resources/application.yaml b/microservices/03-resource-server/src/main/resources/application.yaml new file mode 100644 index 0000000..aa42301 --- /dev/null +++ b/microservices/03-resource-server/src/main/resources/application.yaml @@ -0,0 +1,40 @@ +server: + port: 8052 + address: 127.0.0.1 + servlet: + context-path: /myservices + +# logging: +# level: +# org.springframework.security: trace + +spring: + application: + name: resource-server + + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://127.0.0.1:8051 + + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + username: sa + password: password + h2: + console: + enabled: true + path: /h2-console # H2 Console: http://localhost:8052/myservices/h2-console + jpa: + # database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: update + show-sql: true + defer-datasource-initialization: true # schema.sql and data.sql are executed after Hibernate/JPA auto-creates database tables. + + # Always execute initialization scripts (resources/schema.sql, resources/data.sql) on startup + sql: + init: + mode: always diff --git a/microservices/03-resource-server/src/main/resources/data.sql b/microservices/03-resource-server/src/main/resources/data.sql new file mode 100644 index 0000000..173ee0e --- /dev/null +++ b/microservices/03-resource-server/src/main/resources/data.sql @@ -0,0 +1,8 @@ +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'); + +INSERT INTO post (title, author, description) VALUES ('The rabbit jumped over the moon.', 'Kamal', 'Once upon a time there was a rabbit. And then...'); +INSERT INTO post (title, author, description) VALUES ('The fox jumped over the moon.', 'Kamal', 'Once upon a time there was a fox. And then...'); \ No newline at end of file -- cgit v1.2.3