๐ 1. GateWay๋?
- MSA (Microservices Architecture)์์ Gateway๋ ์ฌ๋ฌ ๋ง์ดํฌ๋ก์๋น์ค๋ฅผ ๋จ์ผ ์ง์ ์ (Entry Point)์ผ๋ก ํตํฉํ๊ณ ๊ด๋ฆฌํ๋ ์ค์ ์ง์ค์ API ๋ผ์ฐํฐ์ด๋ค.
- ์ฃผ๋ก API Gateway๋ผ๊ณ ๋ถ๋ฆฐ๋ค.
๐ ๋จ์ผ ์ง์ ์ ์ค์
*๊ฐ ์๋น์ค port์ ์ง์ ์ ๊ทผ์ด ๋ถ๊ฐ๋ฅํ๋๋ก ๋ฐฉํ๋ฒฝ ๋ฑ ๋คํธ์ํฌ ์ค์ ์ผ๋ก ๊ฐ๋ณ ์๋น์ค ํฌํธ์ ๋ํ ์ธ๋ถ ์ ์์ ์ฐจ๋จ.
*Spring Boot๋ฅผ ์ฌ์ฉํ๋ฉด, server.port=8083 server.address=127.0.0.1์ค์ ํ์ฌ ๋ก์ปฌ์์๋ง ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ์ ํํ ์ // Gateway์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ค์
exchange.getRequest().mutate().header("X-User-Roles", "ROLE_USER,ROLE_ADMIN").build();
์์ต๋๋ค.
โ API Gateway์ ์ญํ
- ์์ฒญ ๋ผ์ฐํ (Request Routing): ํด๋ผ์ด์ธํธ๋ ํ๋์ ์๋ํฌ์ธํธ๋ง ํธ์ถํ๋ฉด ๋๋ฉฐ, Gateway๊ฐ ์ด๋ฅผ ์ ์ ํ ์๋น์ค๋ก ์ ๋ฌํ๋ค.
- ์ธ์ฆ ๋ฐ ์ธ๊ฐ (Authentication & Authorization): ์ธ์ฆ ํ ํฐ ๊ฒ์ฆ์ ์ํํ๋ค.
- ๋ก๋ ๋ฐธ๋ฐ์ฑ (Load Balancing): ์ฌ๋ฌ ์ธ์คํด์ค๊ฐ ๋ฐฐํฌ๋ ๋ง์ดํฌ๋ก์๋น์ค์ ๋ํด ๋ก๋ ๋ฐธ๋ฐ์ฑ์ ์ํํ์ฌ ํธ๋ํฝ์ ๋ถ๋ฐฐํ๋ค.
- API ์กฐํฉ (API Aggregation): ์ฌ๋ฌ ๋ง์ดํฌ๋ก์๋น์ค์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ํ๋์ ์๋ต์ผ๋ก ํด๋ผ์ด์ธํธ์ ์ ๋ฌํ ์ ์๋ค.
- ๋ชจ๋ํฐ๋ง ๋ฐ ๋ก๊น (Monitoring & Logging): ๋ชจ๋ ํธ๋ํฝ์ ๋ชจ๋ํฐ๋งํ๊ณ , ์์ฒญ ๋ฐ ์๋ต์ ๋ํ ๋ก๊น ์ ์ํํ์ฌ ๋ฌธ์ ๋ฅผ ์ถ์ ํ๊ฑฐ๋ ์ฑ๋ฅ์ ๋ถ์ํ ์ ์๋ค.
๐ 2. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฐ ์์กด์ฑ ์ถ๊ฐ
Spring Cloud Gateway๋ฅผ ์ด์ฉํ์ฌ ๊ตฌํํ๋ค.
โ 2-1. Spring Cloud Gateway๋?
Spring Cloud Gateway๋ Spring Boot ์ ํ๋ฆฌ์ผ์ด์ ์์ API Gateway ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์ฃผ๋ก ์ธ๋ถ API ์์ฒญ์ ๋ด๋ถ ์๋น์ค๋ก ๋ผ์ฐํ ํ๋ ์ญํ
- ์ฌ๋ฌ ์๋น์ค ๊ฐ์ ๋ผ์ฐํ , ํํฐ๋ง, ์ธ์ฆ/์ธ๊ฐ ๋ฑ์ ์ฒ๋ฆฌํ๋ ์ญํ ๋ก ๋ง์ ๋์์ฑ์ ์ฒ๋ฆฌํ๋ค.
- โญ WebFlux ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ฏ๋ก, WebMVC์ ์ฐจ์ด๋ฅผ ์ดํดํด์ผ ํ๋ค.
โ 2-2. WebFlux๋?
WebFlux๋ ๋น๋๊ธฐ ๋ฐ ๋ ผ๋ธ๋กํน ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ์คํํฐ
- Spring Cloud Gateway๋ WebFlux๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ค.
- Spring Cloud Gateway๊ฐ ๋ฆฌ์กํฐ๋ธ ํ๋ก๊ทธ๋๋ฐ ๋ชจ๋ธ(Mono, Flux ๋ฑ)์ ํ์ฉํ๋ ๊ตฌ์กฐ
๐ฅ spring-boot-starter-webflux vs spring-boot-starter-web
- spring-boot-starter-webflux: ๋น๋๊ธฐ ๋ฐ ๋ ผ๋ธ๋กํน ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ์คํํฐ
- spring-boot-starter-web: ๋๊ธฐ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ์คํํฐ
โ 2-3. ์์กด์ฑ ์ถ๊ฐ
๐ build.gradle
// implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway' // Spring Cloud
dependencyManagement {
imports {
// Spring Cloud ๊ด๋ จ ์์กด์ฑ๋ค์ ๋ฒ์ ๊ด๋ฆฌ
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2024.0.0"
}
}
๐ 3. Eureka์ ํตํฉ
์ ๋ ์นด ํด๋ผ์ด์ธํธ ์ค์ ์ฐธ๊ณ !
๐ Eureka
โ 3-1. ์ฌ์ฉ ๋ฐฉ๋ฒ
- Eureka ๋ฏธ์ฌ์ฉ
spring.cloud.gateway.routes[0].uri=http://localhost:8081,http://localhost:8082 - Eureka ์ฌ์ฉ
spring.cloud.gateway.routes[0].uri=lb://SERVICE-1
โ ๏ธ ์ฐธ๊ณ
- SERVICE-1์ spring.application.name=service-1 ์ค์ ๋ ์ด๋ฆ์ด๋ค.
- Eureka์ ๋ฑ๋กํ๋ ค๋ฉด application name์ ‘_’๋ฅผ ํฌํจํ๋ฉด ์ ๋๋ค!! (‘-’ ์ฌ์ฉ)
๐ 4. Configuration
โ 4-1. application.properties์์ ์ค์
๐ application.properties
# ๋ผ์ฐํ
ID (์ด ์ค์ ์ ์ด ๋ผ์ฐํ
๊ท์น์ ๊ณ ์ ์๋ณ์)
spring.cloud.gateway.routes[0].id=to_service1
# ์์ฒญ์ ๋ผ์ฐํ
ํ URI (๋ก๋ ๋ฐธ๋ฐ์๋ฅผ ์ฌ์ฉํ์ฌ SERVICE-1์ผ๋ก ๋ผ์ฐํ
)
spring.cloud.gateway.routes[0].uri=lb://SERVICE-1
# ์์ฒญ์ด ๋ค์ด์ฌ ๊ฒฝ๋ก (์: `/service1/**`์ ๋ง๋ ์์ฒญ์ ํด๋น URI๋ก ๋ผ์ฐํ
)
spring.cloud.gateway.routes[0].predicates[0]=Path=/service1/**
# ์์ฒญ ํค๋์ 'X-Gateway-Header'๋ผ๋ ํค์ 'Gateway-Value'๋ผ๋ ๊ฐ์ ์ถ๊ฐ
dpring.cloud.gateway.routes[0].filters[0]=AddRequestHeader=X-Gateway-Header, Gateway-Value
๐ application.yml
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: service1
uri: lb://SERVICE-1
predicates:
- Path=/service1/**
filters:
- StripPrefix=1
๐ ์ ๋ฆฌ
- Gateway๋ MSA์์ ์ค์ํ ์ญํ ์ ์ํํ๋ฉฐ, Spring Cloud Gateway๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋ค.
- Eureka๋ฅผ ํตํด ๋์ ๋ผ์ฐํ ์ ์ค์ ํ ์ ์๋ค.
- application.properties ๋๋ application.yml์ ํ์ฉํ์ฌ ์ ์ฐํ๊ฒ ์ค์ ๊ฐ๋ฅํ๋ค.
โ 4-2. GatewayConfig ์์ฑ
- Application์ค์ ํ์ผ์ด Configuration๋ก ์ค์ ํ ์ ๋ ์๋ค.
- ์ถ๊ฐ ๋ก์ง์ด ํ์ํ ๋๋ ๋๋ฒ๊น ์ ์ฉ์ดํ๊ฒ ํ๊ธฐ์ํด ์ฌ์ฉ.
๐ GatewayConfig.java: ๊ธฐ๋ณธ ํํ
import org.springframework.cloud.gateway.config.EnableGateway;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// ์ํ ์๋น์ค
.route("product_route", r -> r.path("/product/api/products/all")
.filters(f -> f.stripPrefix(1))
.uri("lb://product-service"))
// ์ํ ์๋น์ค - ๊ถํ ํ์
.route("product_route_auth_user", r -> r.path("/product/**")
.filters(f -> f.stripPrefix(1))
.filter((exchange, chain) -> { // โญ ์กฐ๊ฑด์ ๋ฐ๋ผ ์์ฒญ ์ฐจ๋จ ๊ฐ๋ฅ
List<String> roles = exchange.getAttribute("roles");
if (roles != null && roles.contains("ADMIN")) {
return chain.filter(exchange); // ์์ฒญ์ ๋ค์ ํํฐ๋ก ์ ๋ฌ
} else {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
})
.uri("lb://product-service"))
.build();
}
}
โญ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ Filter๋ฅผ ์ ์ฉํ๊ณ ์ถ๋ค๋ฉด?
๐ฅ GatewayFilter ์ข ๋ฅ
ํน์ฑ | GatewayFilter | GatewayFilterFactory |
์ค์ ๋ฐฉ์ | ์์ฑ์ ์ฃผ์ ์ผ๋ก ํํฐ์ ๋์์ ์ค์ | Config ํด๋์ค๋ฅผ ํตํด ๋์ ์ผ๋ก ์ค์ ๊ฐ๋ฅ |
๋์ ํ๋ผ๋ฏธํฐ | ์์ฑ์์์ ์ ์ ๊ฐ์ผ๋ก ์ค์ | apply ๋ฉ์๋์์ ๋์ ์ผ๋ก ํ๋ผ๋ฏธํฐ ์ค์ |
์ ์ฐ์ฑ | ์ ์ ์ธ ๊ฐ์ ์์กด | ๋ค์ํ ์ค์ ๊ฐ์ ๋์ ์ผ๋ก ์ฒ๋ฆฌ ๊ฐ๋ฅ |
์ฃผ ์ฉ๋ | ๊ณ ์ ๋ ๋์์ ํ๋ ํํฐ ๊ตฌํ | ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ณ ๋์ ์ผ๋ก ํ๋ผ๋ฏธํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ํํฐ |
โญ ์๋ ์์๋ jwt ๊ฒ์ฆ ํํฐ์ธ๋ฐ Spring Security์์ ๊ฒ์ฆ ํ ๊ฒฝ์ฐ gateway filter์์๋ ๊ฒ์ฆํ ํ์ ์๋ค!!
๐ GatewayConfig.java: GatewayFilter ํํฐ๋ฅผ ์ ์ฉ
import com.example.gateway.filter.JwtGatewayFilter;
@Configuration
public class GatewayConfig {
private final JwtGatewayFilter jwtGatewayFilter;
public GatewayConfig(JwtGatewayFilter jwtGatewayFilter) {
this.jwtGatewayFilter = jwtGatewayFilter;
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// ์ํ ์กฐํ ์๋น์ค (์ธ์ฆ ์๋ต)
.route("product_route", r -> r.path("/product/api/products/all")
.filters(f -> f.stripPrefix(1))
.uri("lb://product-service"))
// ์ํ ์๋น์ค (์ธ์ฆ ํ์)
.route("product_service", r -> r.path("/product/**")
.filters(f -> f.stripPrefix(1)
.filter(jwtGatewayFilter))
.uri("lb://product-service"))
.build();
}
}
๐ JwtGatewayFilter.java: GatewayFilter
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.example.gateway.util.JwtTokenProvider;
import reactor.core.publisher.Mono;
@Component
public class JwtGatewayFilter implements GatewayFilter {
private final JwtTokenProvider jwtTokenProvider;
public JwtGatewayFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = extractTokenFromRequest(exchange);
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
List<String> roles = jwtTokenProvider.getRolesFromToken(token);
// ๊ถํ ์ฒดํฌ
// if (requiredRole != null && !roles.contains(requiredRole)) {
// //log.info("๊ถํ ๋ถ์กฑ: ํ์ํ ์ญํ = {}, ์ฌ์ฉ์์ ์ญํ = {}", requiredRole, roles);
// exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// return exchange.getResponse().setComplete();
// }
/* ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ */
// 1. ์์ฑ ๋งต์ ์ถ๊ฐ: gateway ๋ด ์์ฒญ-์๋ต ํ๋ฆ ๋ด์ ๋ก์ง์์๋ง ์ฌ์ฉ
// exchange.getAttributes().put("username", username);
// exchange.getAttributes().put("roles", roles);
// 2. ํค๋ ์ถ๊ฐ: ํ์ ์๋น์ค๋ ์ธ๋ถ API์๋ ๋ฐ์
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.header("X-User-Id", username)
.header("X-User-Roles", String.join(", ", roles))
.build();
exchange = exchange.mutate().request(modifiedRequest).build();
// 3. SecurityContext์ ์ค์ : gateWay์ชฝ springSecurity๋ง ์ ์ฉ๋๋๊ฑฐ๋ผ ์ฌ์ฉ์ ์๋น์ค๋ ์ฐ๋x
// List<GrantedAuthority> authorities = roles.stream()
// .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // ROLE_ prefix ์ถ๊ฐ
// .collect(Collectors.toList());
// Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
// SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
private String extractTokenFromRequest(ServerWebExchange exchange) {
String bearerToken = exchange.getRequest().getHeaders().getFirst("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
๐ GatewayConfig.java: GatewayFilterFactory ํํฐ๋ฅผ ์ ์ฉ
import com.example.gateway.filter.JwtGatewayFilterFactory;
@Configuration
public class GatewayConfig {
private JwtGatewayFilterFactory jwtGatewayFilterFactory;
public GatewayConfig(JwtGatewayFilterFactory jwtGatewayFilterFactory) {
this.jwtGatewayFilterFactory = jwtGatewayFilterFactory;
}
// ๊ณตํต ๋ฉ์๋: ์ญํ ์ ์ค์ ํ๋ ๋ฉ์๋
private JwtGatewayFilterFactory.Config createConfigWithRole(String role) {
JwtGatewayFilterFactory.Config config = new JwtGatewayFilterFactory.Config();
config.setRequiredRole(role);
return config;
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// ์ํ ์๋น์ค
.route("product_route", r -> r.path("/product/api/products/all")
.filters(f -> f.stripPrefix(1))
.uri("lb://product-service"))
.route("product_route_auth_user", r -> r.path("/product/**")
.filters(f -> f.stripPrefix(1)
.filter(jwtGatewayFilterFactory.apply(createConfigWithRole("USER")))
)
.uri("lb://product-service"))
.build();
}
}
๐ JwtGatewayFilterFactory.java: GatewayFilterFactory
import java.util.List;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.example.gateway.util.JwtTokenProvider;
@Component
public class JwtGatewayFilterFactory extends AbstractGatewayFilterFactory<JwtGatewayFilterFactory.Config> {
private final JwtTokenProvider jwtTokenProvider;
public JwtGatewayFilterFactory(JwtTokenProvider jwtTokenProvider) {
super(Config.class);
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String token = extractTokenFromRequest(exchange);
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
List<String> roles = jwtTokenProvider.getRolesFromToken(token);
// ๊ถํ ์ฒดํฌ
if (config.getRequiredRole() != null && !roles.contains(config.getRequiredRole())) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); //403
return exchange.getResponse().setComplete();
}
// ํค๋ ์ถ๊ฐ: ํ์ ์๋น์ค๋ ์ธ๋ถ API์๋ ๋ฐ์
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.header("X-User-Name", username)
.header("X-User-Roles", String.join(", ", roles))
.build();
exchange = exchange.mutate().request(modifiedRequest).build();
} else {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //401
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
};
}
public static class Config {
private String requiredRole;
public String getRequiredRole() {
return requiredRole;
}
public void setRequiredRole(String requiredRole) {
this.requiredRole = requiredRole;
}
}
private String extractTokenFromRequest(ServerWebExchange exchange) {
String bearerToken = exchange.getRequest().getHeaders().getFirst("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
'Backend > spring cloud (MSA)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
์ค์๊ฐ ๋ฐ์ดํฐ ์ ์ก (0) | 2025.02.23 |
---|---|
[Kafka] ๊ณ ๊ธ ์ค์ (0) | 2025.02.23 |
Kafka๋? (0) | 2025.02.23 |
Eureka๋? (0) | 2025.02.16 |
Spring Cloud๋? (0) | 2025.02.16 |