diff options
| author | Kamal Wickramanayake <kamal@inbox.lk> | 2026-07-03 19:02:36 +0530 |
|---|---|---|
| committer | Kamal Wickramanayake <kamal@inbox.lk> | 2026-07-03 19:02:36 +0530 |
| commit | aa122113ade36f02dc8fdbebdf1232b5c4b8742c (patch) | |
| tree | c345b83db6c769bf5e72a09e3f19d43431961695 /microservices/02-api-gateway/src/main/java | |
| parent | b221a83ecef1dd7f9583d5107017d24e668205a6 (diff) | |
Microservice sample projects
Diffstat (limited to 'microservices/02-api-gateway/src/main/java')
5 files changed, 227 insertions, 0 deletions
diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java new file mode 100644 index 0000000..bdf050a --- /dev/null +++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java @@ -0,0 +1,13 @@ +package com.example.apigateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiGatewayApplication.class, args); + } + +} diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/RoleHeaderRelayFilter.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/RoleHeaderRelayFilter.java new file mode 100644 index 0000000..e3bb241 --- /dev/null +++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/RoleHeaderRelayFilter.java @@ -0,0 +1,69 @@ +package com.example.apigateway.config; + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.stream.Collectors; + +/** + * Once the roles are part of the Gateway's session principal, you often need to + * forward them down to your resource microservices. You can do this by creating + * a GlobalFilter that reads the authenticated roles and places them into custom + * HTTP headers. + * + * RoleHeaderRelayFilter + */ +@Component +public class RoleHeaderRelayFilter implements GlobalFilter, Ordered { + + @Override + public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .flatMap(authentication -> { + if (authentication != null && authentication.getPrincipal() instanceof OAuth2User) { + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); + + // Extract username (adjust the attribute key based on your IdP provider) + String username = oAuth2User.getAttribute("preferred_username"); + if (username == null) { + username = oAuth2User.getName(); + } + + // Extract roles/authorities + String roles = oAuth2User.getAuthorities().stream() + .map(grantedAuthority -> grantedAuthority.getAuthority()) + .collect(Collectors.joining(",")); + + // Mutate the request with new headers + ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() + .header("X-User-Roles", roles) + .header("X-User-Name", username) // Changed 'U-User-Name' to standard 'X-' format for consistency, or use your exact key + .build(); + + // Proceed with the mutated exchange + return chain.filter(exchange.mutate().request(mutatedRequest).build()); + } + + // If not authenticated via OAuth2User, just pass through + return chain.filter(exchange); + }) + .switchIfEmpty(chain.filter(exchange)); // Handle cases where SecurityContext is empty + } + + @Override + public int getOrder() { + // Run after the Security Web Filter loads the context, but before routing + return Ordered.LOWEST_PRECEDENCE - 1; + } +} diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/SecurityConfig.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/SecurityConfig.java new file mode 100644 index 0000000..c503032 --- /dev/null +++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/config/SecurityConfig.java @@ -0,0 +1,93 @@ +package com.example.apigateway.config; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; +import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; + +import com.example.apigateway.security.dto.UserResonseDto; + +import reactor.core.publisher.Mono; + +/** + * Ref: + * https://docs.spring.io/spring-security/reference/reactive/oauth2/login/advanced.html + */ +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + + private final WebClient webClient = WebClient.create(); // WebClient configured to point to your backend service + + @Value("${app.userRolesUrl}") + private String userRolesUrl; + + @Bean + public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() { + final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService(); + + return (userRequest) -> { + // Delegate to the default implementation for loading a user + return delegate.loadUser(userRequest) + .flatMap((oidcUser) -> { + String username = oidcUser.getAttribute("sub"); + + return fetchRolesFromBackend(username) + .map(roles -> { + Set<GrantedAuthority> authorities = roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())) + .collect(Collectors.toSet()); + + // Keep original identity scopes if needed + authorities.addAll(oidcUser.getAuthorities()); + + ProviderDetails providerDetails = userRequest.getClientRegistration() + .getProviderDetails(); + String userNameAttributeName = providerDetails.getUserInfoEndpoint() + .getUserNameAttributeName(); + if (StringUtils.hasText(userNameAttributeName)) { + return new DefaultOidcUser(authorities, oidcUser.getIdToken(), + oidcUser.getUserInfo(), userNameAttributeName); + } else { + return new DefaultOidcUser(authorities, oidcUser.getIdToken(), + oidcUser.getUserInfo()); + } + }); + }); + }; + } + + private Mono<List<String>> fetchRolesFromBackend(String username) { + Mono<List<String>> response = this.webClient.get() + .uri(userRolesUrl, username) + .retrieve() + .bodyToMono(UserResonseDto.class) + .map(UserResonseDto::getRoles) + .onErrorReturn(List.of("")); // Can return a String indicating a fallback role on backend failure + + return response; + } +} diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/controllers/UserRestController.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/controllers/UserRestController.java new file mode 100644 index 0000000..5d6581c --- /dev/null +++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/controllers/UserRestController.java @@ -0,0 +1,36 @@ +package com.example.apigateway.security.controllers; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import reactor.core.publisher.Mono; + +@RestController +public class UserRestController { + + @GetMapping("/api/security/me") + public Mono<Map<String, Object>> getLoggedInUser(@AuthenticationPrincipal OAuth2User principal) { + if (principal == null) { + return Mono.just(Map.of()); + } + + // Filter roles starting with "ROLE_" + List<String> roles = principal.getAuthorities().stream() + .map(grantedAuthority -> grantedAuthority.getAuthority()) + .filter(authority -> authority.startsWith("ROLE_")) + .map(s -> s.substring(5)) // Cut ROLE_ part + .collect(Collectors.toList()); + + return Mono.just(Map.of( + "username", principal.getAttribute("sub"), + // "name", principal.getName(), + // "email", principal.getEmail(), + "roles", roles)); + } +} diff --git a/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/dto/UserResonseDto.java b/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/dto/UserResonseDto.java new file mode 100644 index 0000000..d553d9a --- /dev/null +++ b/microservices/02-api-gateway/src/main/java/com/example/apigateway/security/dto/UserResonseDto.java @@ -0,0 +1,16 @@ +package com.example.apigateway.security.dto; + +import java.util.List; + +public class UserResonseDto { + private List<String> roles; + + // Getters and Setters + public List<String> getRoles() { + return roles; + } + + public void setRoles(List<String> roles) { + this.roles = roles; + } +} |
