Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,19 @@ ext {
}

dependencies {
//webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
//gateway
implementation 'org.springframework.cloud:spring-cloud-starter-gateway-server-webflux'
//config
implementation 'org.springframework.cloud:spring-cloud-starter-config'

//Spring Security & JWT
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/irum/apigateway/config/PropertiesConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.irum.apigateway.config;

import com.irum.apigateway.properties.GatewayPathProperties;
import com.irum.apigateway.properties.JwtProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@EnableConfigurationProperties({JwtProperties.class, GatewayPathProperties.class})
@Configuration
public class PropertiesConfig {}
64 changes: 64 additions & 0 deletions src/main/java/com/irum/apigateway/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.irum.apigateway.config;

import com.irum.apigateway.filter.JwtAuthenticationFilter;
import com.irum.apigateway.properties.GatewayPathProperties;
import lombok.RequiredArgsConstructor;
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.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final GatewayPathProperties paths;
private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.csrf(ServerHttpSecurity.CsrfSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeExchange(
exchange -> {
paths.getPublicEndpoints()
.forEach(
route -> {
route.getMethods()
.forEach(
httpMethod -> {
exchange.pathMatchers(
httpMethod,
route.getPath())
.permitAll();
});
});
exchange.anyExchange().denyAll();
});
return http.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addExposedHeader("Authorization");
config.addAllowedOrigin("http://localhost:8080");
config.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.irum.apigateway.filter;

import com.irum.apigateway.properties.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import javax.crypto.SecretKey;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter implements WebFilter {

private final JwtProperties jwtProperties;

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return chain.filter(exchange);
}

String token = authHeader.substring(7);
try {
SecretKey key =
Keys.hmacShaKeyFor(
jwtProperties.accessTokenSecret().getBytes(StandardCharsets.UTF_8));
Claims claims =
Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload();
String memberId = claims.getSubject();
String role = claims.get("TOKEN_ROLE_NAME", String.class);
GrantedAuthority authority = new SimpleGrantedAuthority(role);
Collection<GrantedAuthority> authorities = Collections.singletonList(authority);

UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(memberId, null, authorities);
ServerHttpRequest mutatedRequest =
exchange.getRequest().mutate().header("X-Member-Id", memberId).build();

ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();

return chain.filter(mutatedExchange)
.contextWrite(
ReactiveSecurityContextHolder.withSecurityContext(
Mono.just(new SecurityContextImpl(auth))));

} catch (JwtException e) {
log.warn("JWT Validation failed: {}", e.getMessage());
return chain.filter(exchange);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.irum.apigateway.properties;

import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.HttpMethod;

@Data
@RefreshScope
@ConfigurationProperties(prefix = "security.endpoints")
public class GatewayPathProperties {

private List<Route> publicEndpoints;
private List<Route> customerEndpoints;
private List<Route> ownerEndpoints;
private List<Route> managerEndpoints;
private List<Route> masterEndpoints;

@Data
public static class Route {
private String path;
private List<HttpMethod> methods;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.irum.apigateway.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "security.jwt")
// @RefreshScope
public record JwtProperties(String accessTokenSecret) {}
7 changes: 7 additions & 0 deletions src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
spring:
config:
activate:
on-profile: "local"

server:
port: ${PORT:8080}
20 changes: 20 additions & 0 deletions src/main/resources/application-security.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
spring:
config:
activate:
on-profile: "security"
security:
endpoints:
public-endpoints:
- path: /member-service/**
methods: [POST, GET, PUT, PATCH, DELETE]
customer-endpoints:
- path: /**
methods: [POST, GET, PUT, PATCH, DELETE]
manager-endpoints:
- path: /**
methods: [POST, GET, PUT, PATCH, DELETE]
master-endpoints:
- path: /**
methods: [POST, GET, PUT, PATCH, DELETE]
jwt:
access-token-secret: ${JWT_ACCESS_TOKEN_SECRET}
7 changes: 6 additions & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
spring:
application:
name: api-gateway
profiles:
active: local
include:
- routes
- security

config:
import: "optional:configserver:${CONFIG_SERVER_URL:localhost:8888}"
import: "optional:configserver:${CONFIG_SERVER_URL:http://localhost:8888}"