GatewaySecurityConfig.java
package com.ecommerce.gateway.security;
import com.ecommerce.platform.common.api.ApiResponse;
import com.ecommerce.gateway.filter.RequestIdGlobalFilter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@Configuration
@EnableWebFluxSecurity
public class GatewaySecurityConfig {
@Bean
public SecretKey gatewayJwtSecretKey(GatewayTokenProperties properties) {
return new SecretKeySpec(
properties.secret().getBytes(StandardCharsets.UTF_8),
"HmacSHA256");
}
@Bean
public ReactiveJwtDecoder gatewayJwtDecoder(
SecretKey gatewayJwtSecretKey,
GatewayTokenProperties properties) {
NimbusReactiveJwtDecoder decoder = NimbusReactiveJwtDecoder
.withSecretKey(gatewayJwtSecretKey)
.macAlgorithm(MacAlgorithm.HS256)
.build();
decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(properties.issuer()));
return decoder;
}
@Bean
public SecurityWebFilterChain gatewaySecurityWebFilterChain(
ServerHttpSecurity http,
ReactiveJwtDecoder gatewayJwtDecoder,
ObjectMapper objectMapper) {
JwtGrantedAuthoritiesConverter authorities = new JwtGrantedAuthoritiesConverter();
authorities.setAuthoritiesClaimName("roles");
authorities.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authorities);
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.cors(Customizer.withDefaults())
.requestCache(ServerHttpSecurity.RequestCacheSpec::disable)
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange(authorize -> authorize
.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.pathMatchers(
"/actuator/health",
"/actuator/health/**"
).permitAll()
.pathMatchers(
"/api/v1/identity/internal/**",
"/api/v1/inventory/internal/**",
"/api/v1/trade/internal/**",
"/api/v1/marketing/internal/**"
).denyAll()
.pathMatchers(
"/api/v1/identity/status",
"/api/v1/catalog/status",
"/api/v1/inventory/status",
"/api/v1/trade/status",
"/api/v1/payment/status",
"/api/v1/fulfillment/status",
"/api/v1/marketing/status",
"/api/v1/chat/status",
"/api/v1/notifications/status",
"/api/v1/analytics/status"
).permitAll()
.pathMatchers(HttpMethod.GET,
"/api/v1/trade/status/distributed-id",
"/api/v1/catalog/categories",
"/api/v1/catalog/brands",
"/api/v1/catalog/products",
"/api/v1/catalog/products/**",
"/api/v1/catalog/search/products",
"/api/v1/inventory/stocks/**",
"/api/v1/marketing/flash-sales/*"
).permitAll()
.pathMatchers(HttpMethod.POST,
"/api/v1/identity/auth/register",
"/api/v1/identity/auth/login",
"/api/v1/identity/auth/refresh",
"/api/v1/identity/auth/logout",
"/api/v1/payment/callbacks/mock",
"/api/v1/payment/callbacks/mock/refunds"
).permitAll()
.pathMatchers("/ws/chat").permitAll()
.pathMatchers("/api/v1/catalog/admin/**")
.hasAnyRole("ADMIN", "OPERATOR")
.pathMatchers("/api/v1/inventory/admin/**")
.hasAnyRole("ADMIN", "WAREHOUSE")
.pathMatchers("/api/v1/trade/admin/**").hasRole("ADMIN")
.pathMatchers("/api/v1/payment/admin/**").hasRole("ADMIN")
.pathMatchers(
"/api/v1/fulfillment/admin/orders/*/exception/resolve"
).hasRole("ADMIN")
.pathMatchers("/api/v1/fulfillment/admin/**")
.hasAnyRole("ADMIN", "WAREHOUSE")
.pathMatchers("/api/v1/marketing/admin/**")
.hasAnyRole("ADMIN", "OPERATOR")
.pathMatchers(
"/api/v1/chat/admin/**",
"/api/v1/chat/conversations/*/claim"
).hasAnyRole("ADMIN", "OPERATOR")
.pathMatchers("/api/v1/notifications/admin/**")
.hasAnyRole("ADMIN", "OPERATOR")
.pathMatchers("/api/v1/analytics/**")
.hasAnyRole("ADMIN", "OPERATOR")
.pathMatchers(
"/api/v1/catalog/review-eligibilities",
"/api/v1/catalog/reviews",
"/api/v1/catalog/reviews/**"
).hasRole("CUSTOMER")
.pathMatchers("/api/v1/trade/**", "/api/v1/payment/**")
.hasAnyRole("CUSTOMER", "ADMIN")
.pathMatchers("/api/v1/marketing/**")
.hasAnyRole("CUSTOMER", "ADMIN")
.pathMatchers("/api/v1/chat/**")
.hasAnyRole("CUSTOMER", "ADMIN", "OPERATOR")
.anyExchange().authenticated())
.oauth2ResourceServer(resource -> resource
.jwt(jwt -> jwt
.jwtDecoder(gatewayJwtDecoder)
.jwtAuthenticationConverter(
new ReactiveJwtAuthenticationConverterAdapter(converter)))
.authenticationEntryPoint((exchange, exception) ->
writeFailure(
exchange,
objectMapper,
HttpStatus.UNAUTHORIZED,
"UNAUTHORIZED",
"Authentication is required")))
.exceptionHandling(exceptions -> exceptions
.accessDeniedHandler((exchange, exception) -> {
if (isInternalPath(exchange)) {
return writeFailure(
exchange,
objectMapper,
HttpStatus.NOT_FOUND,
"NOT_FOUND",
"Resource not found");
}
return writeFailure(
exchange,
objectMapper,
HttpStatus.FORBIDDEN,
"FORBIDDEN",
"Access is denied");
}))
.build();
}
private boolean isInternalPath(ServerWebExchange exchange) {
return Arrays.stream(exchange.getRequest().getPath().pathWithinApplication()
.value().split("/"))
.anyMatch("internal"::equals);
}
private Mono<Void> writeFailure(
ServerWebExchange exchange,
ObjectMapper objectMapper,
HttpStatus status,
String code,
String message) {
byte[] body;
try {
body = objectMapper.writeValueAsBytes(ApiResponse.failure(code, message));
} catch (JsonProcessingException exception) {
body = ("{\"code\":\"" + code + "\",\"message\":\"" + message + "\"}")
.getBytes(StandardCharsets.UTF_8);
}
exchange.getResponse().setStatusCode(status);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
exchange.getResponse().getHeaders().set(
RequestIdGlobalFilter.REQUEST_ID_HEADER,
RequestIdGlobalFilter.resolveRequestId(
exchange.getRequest().getHeaders().getFirst(
RequestIdGlobalFilter.REQUEST_ID_HEADER)));
return exchange.getResponse().writeWith(Mono.just(
exchange.getResponse().bufferFactory().wrap(body)));
}
}