summaryrefslogtreecommitdiff
path: root/microservices/03-resource-server/src/main
diff options
context:
space:
mode:
authorKamal Wickramanayake <kamal@inbox.lk>2026-07-03 19:02:36 +0530
committerKamal Wickramanayake <kamal@inbox.lk>2026-07-03 19:02:36 +0530
commitaa122113ade36f02dc8fdbebdf1232b5c4b8742c (patch)
treec345b83db6c769bf5e72a09e3f19d43431961695 /microservices/03-resource-server/src/main
parentb221a83ecef1dd7f9583d5107017d24e668205a6 (diff)
Microservice sample projects
Diffstat (limited to 'microservices/03-resource-server/src/main')
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/ResourceServerApplication.java13
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/config/SecurityConfig.java32
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/post/Post.java56
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRepository.java8
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostRestController.java22
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/post/PostService.java18
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/security/RolesResponse.java7
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/security/User.java110
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRepository.java11
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserRestController.java71
-rw-r--r--microservices/03-resource-server/src/main/java/com/example/resourceserver/security/UserService.java47
-rw-r--r--microservices/03-resource-server/src/main/resources/application.yaml40
-rw-r--r--microservices/03-resource-server/src/main/resources/data.sql8
13 files changed, 443 insertions, 0 deletions
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<Post, Long> {
+} \ 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<Post> 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<Post> 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<String> 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<String> 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<String> getRoles() {
+ return roles;
+ }
+
+ public void setRoles(Set<String> 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<User, Long> {
+ Optional<User> 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<RolesResponse> getRoles(@PathVariable String username) {
+ Optional<User> 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<String> getRoles(Authentication authentication) {
+ return authentication.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.toList());
+ }
+
+ // Inspect raw JWT claims directly
+ @GetMapping("/test-roles-jwt")
+ public Map<String, Object> 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<User> findByUsername(String username) {
+ return userRepository.findByUsername(username);
+ }
+
+ public Optional<User> findById(Long id) {
+ return userRepository.findById(id);
+ }
+
+ public List<User> 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<User> 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