부트캠프 프로젝트의 "동상이농"에서 Spring Cloud 프레임워크를 이용하여 MSA 프로젝트 세팅을 맡았는데요
제가 MSA 세팅을 하며 CORS 설정에 대해 놓쳤던 부분과 새롭게 알게된 것에 대해 포스팅해보려고 합니다.
동상이농의 MSA 아키텍처
동상이농 프로젝트는 아래와 같은 MSA 구조를 가지고 있습니다.
8081 포트를 사용하고 있는 vue 프로젝트가 8080 포트를 사용하고 있는 API Gateway에게 요청을 보내면
API Gateway는 서비스 디스커버리 역할을 하는 Eureka 서버를 통해 각 모듈의 IP, Port 정보를 알아내고 적절한 곳으로 라우팅하는 것이죠.
(API gateway와 Eureka를 제외한 서버들은 유동포트를 쓰고 있기 때문에, Eureka를 통해 IP, Port를 알아내는 것입니다.
참고로 Eureka 서버는 8761 포트를 사용하고 있습니다.)
MSA에 대한 이해가 부족한 채로 프로젝트 세팅을 시작했던 탓에, 저는 API Gateway가 요청을 받으면 각 모듈로 다시 요청을 보내주는 것이라고 생각했습니다. (미리 결론부터 말하자면 이렇지 않습니다!)
따라서 각 프로젝트에 대한 CORS 설정을 다음과 같이 해주었는데요.
API GateWay의 CORS 설정
API 게이트웨이는 클라이언트 프로젝트로부터 요청을 받기 때문에 localhost:8081에 대한 CORS 설정을 추가해주면 된다고 생각했습니다.
따라서 application-local.yml에 다음과 같이 cors 설정을 해주었습니다.
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: 'http://localhost:8081'
allow-credentials: true
allowedHeaders: '*'
allowedMethods:
- PUT
- GET
- POST
- DELETE
- OPTIONS
Member, Product, Order, Live 모듈의 Cors 설정
실제 서비스 기능을 담당하는 위 4개의 모듈은 API 게이트웨이로부터 요청을 받는다고 생각했기 때문에 localhost:8080에 대한 CORS를 허용해주었습니다.
package org.samtuap.inong.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*");
// .allowCredentials(true);
}
};
}
}
403 Error
이렇게 설정해 준 뒤, 프론트엔드에서 axios 요청을 보내니 403 응답이 반환되는 것을 확인할 수 있었습니다.
처음에는 게이트웨이 쪽에서 요청이 막혔다고 생각해서 Spring Cloud Gateway와 cors 설정에 대한 레퍼런스를 매우 많이 찾아보았었는 데요.
아무리 자료를 뒤져보아도 게이트웨이의 application.yml에 해준 설정에는 문제가 없어보였습니다.
삽질의 흔적들:
https://github.com/spring-cloud/spring-cloud-gateway/issues/2946
https://github.com/spring-cloud/spring-cloud-gateway/issues/3205
https://github.com/spring-cloud/spring-cloud-gateway/issues/3435
결국 gateway의 로그 레벨을 trace로 바꾸어서 로그를 찍어봤는데
2024-09-26T22:26:25.730+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [73f1a520, L:/[0:0:0:0:0:0:0:1]:8080 - R:/[0:0:0:0:0:0:0:1]:50467] New http connection, requesting read
2024-09-26T22:26:25.731+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.netty.transport.TransportConfig : [73f1a520, L:/[0:0:0:0:0:0:0:1]:8080 - R:/[0:0:0:0:0:0:0:1]:50467] Initialized pipeline DefaultChannelPipeline{(reactor.left.httpCodec = io.netty.handler.codec.http.HttpServerCodec), (reactor.left.httpTrafficHandler = reactor.netty.http.server.HttpTrafficHandler), (reactor.right.reactiveBridge = reactor.netty.channel.ChannelOperationsHandler)}
2024-09-26T22:26:25.775+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [73f1a520, L:/[0:0:0:0:0:0:0:1]:8080 - R:/[0:0:0:0:0:0:0:1]:50467] Increasing pending responses count: 1
2024-09-26T22:26:25.777+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] reactor.netty.http.server.HttpServer : [73f1a520-1, L:/[0:0:0:0:0:0:0:1]:8080 - R:/[0:0:0:0:0:0:0:1]:50467] Handler is being applied: org.springframework.http.server.reactive.ReactorHttpHandlerAdapter@7096d15b
2024-09-26T22:26:25.779+09:00 TRACE 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.g.f.WeightCalculatorWebFilter : Weights attr: {}
2024-09-26T22:26:25.780+09:00 TRACE 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/product-service/**]" does not match against value "/member-service/member/health/check"
2024-09-26T22:26:25.780+09:00 TRACE 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "/member-service/**" matches against value "/member-service/member/health/check"
2024-09-26T22:26:25.780+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping : Route matched: member-service
2024-09-26T22:26:25.781+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping : Mapping [Exchange: GET http://localhost:8080/member-service/member/health/check] to Route{id='member-service', uri=lb://member-service, order=0, predicate=Paths: [/member-service/**], match trailing slash: true, gatewayFilters=[[[DedupeResponseHeader Access-Control-Allow-Origin Access-Control-Allow-Credentials = RETAIN_FIRST], order = 1], [[StripPrefix parts = 1], order = 1]], metadata={}}
2024-09-26T22:26:25.781+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping : [73f1a520-2] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@6fedb3dc
2024-09-26T22:26:25.782+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.g.handler.FilteringWebHandler : Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@3a70575}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@64420e34}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@356341c9}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@286a4c52}, order = 0], [[DedupeResponseHeader Access-Control-Allow-Origin Access-Control-Allow-Credentials = RETAIN_FIRST], order = 1], [[StripPrefix parts = 1], order = 1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@5d96d434}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@b77b0a0}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerServiceInstanceCookieFilter@15be68b}, order = 10151], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@13dd7887}, order = 2147483646], GatewayFilterAdapter{delegate=org.samtuap.inong.securities.JwtGlobalFilter@7221539}, [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@22ff1372}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@7283877}, order = 2147483647]]
2024-09-26T22:26:25.783+09:00 TRACE 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.g.filter.RouteToRequestUrlFilter : RouteToRequestUrlFilter start
2024-09-26T22:26:25.783+09:00 TRACE 68033 --- [api-gateway] [ctor-http-nio-3] s.c.g.f.ReactiveLoadBalancerClientFilter : ReactiveLoadBalancerClientFilter url before: lb://member-service/member/health/check
2024-09-26T22:26:25.785+09:00 TRACE 68033 --- [api-gateway] [ctor-http-nio-3] s.c.g.f.ReactiveLoadBalancerClientFilter : LoadBalancerClientFilter url chosen: http://192.168.10.16:64756/member/health/check
2024-09-26T22:26:25.792+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.resources.PooledConnectionProvider : [f3cf8307] Created a new pooled channel, now: 0 active connections, 0 inactive connections and 0 pending acquire requests.
2024-09-26T22:26:25.792+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.netty.transport.TransportConfig : [f3cf8307] Initialized pipeline DefaultChannelPipeline{(reactor.left.httpCodec = io.netty.handler.codec.http.HttpClientCodec), (reactor.right.reactiveBridge = reactor.netty.channel.ChannelOperationsHandler)}
2024-09-26T22:26:25.796+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.netty.transport.TransportConnector : [f3cf8307] Connecting to [/192.168.10.16:64756].
2024-09-26T22:26:25.796+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.r.DefaultPooledConnectionProvider : [f3cf8307, L:/192.168.10.16:50468 - R:/192.168.10.16:64756] Registering pool release on close event for channel
2024-09-26T22:26:25.796+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.resources.PooledConnectionProvider : [f3cf8307, L:/192.168.10.16:50468 - R:/192.168.10.16:64756] Channel connected, now: 1 active connections, 0 inactive connections and 0 pending acquire requests.
2024-09-26T22:26:25.796+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.r.DefaultPooledConnectionProvider : [f3cf8307, L:/192.168.10.16:50468 - R:/192.168.10.16:64756] onStateChange(PooledConnection{channel=[id: 0xf3cf8307, L:/192.168.10.16:50468 - R:/192.168.10.16:64756]}, [connected])
2024-09-26T22:26:25.796+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.r.DefaultPooledConnectionProvider : [f3cf8307-1, L:/192.168.10.16:50468 - R:/192.168.10.16:64756] onStateChange(GET{uri=null, connection=PooledConnection{channel=[id: 0xf3cf8307, L:/192.168.10.16:50468 - R:/192.168.10.16:64756]}}, [configured])
2024-09-26T22:26:25.796+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.netty.http.client.HttpClientConnect : [f3cf8307-1, L:/192.168.10.16:50468 - R:/192.168.10.16:64756] Handler is being applied: {uri=http://192.168.10.16:64756/member/health/check, method=GET}
2024-09-26T22:26:25.796+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.r.DefaultPooledConnectionProvider : [f3cf8307-1, L:/192.168.10.16:50468 - R:/192.168.10.16:64756] onStateChange(GET{uri=/member/health/check, connection=PooledConnection{channel=[id: 0xf3cf8307, L:/192.168.10.16:50468 - R:/192.168.10.16:64756]}}, [request_prepared])
2024-09-26T22:26:25.796+09:00 TRACE 68033 --- [api-gateway] [ctor-http-nio-3] o.s.c.gateway.filter.NettyRoutingFilter : outbound route: f3cf8307, inbound: [73f1a520-2]
2024-09-26T22:26:25.796+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] reactor.netty.channel.FluxReceive : [73f1a520-1, L:/[0:0:0:0:0:0:0:1]:8080 - R:/[0:0:0:0:0:0:0:1]:50467] [terminated=true, cancelled=false, pending=0, error=null]: subscribing inbound receiver
2024-09-26T22:26:25.797+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.r.DefaultPooledConnectionProvider : [f3cf8307-1, L:/192.168.10.16:50468 - R:/192.168.10.16:64756] onStateChange(GET{uri=/member/health/check, connection=PooledConnection{channel=[id: 0xf3cf8307, L:/192.168.10.16:50468 - R:/192.168.10.16:64756]}}, [request_sent])
2024-09-26T22:26:25.802+09:00 DEBUG 68033 --- [api-gateway] [ctor-http-nio-3] r.n.http.client.HttpClientOperations : [f3cf8307-1, L:/192.168.10.16:50468 - R:/192.168.10.16:64756] Received response (auto-read:false) : RESPONSE(decodeResult: success, version: HTTP/1.1)
위 사진의 빨간 박스를 보고 요청이 gateway를 잘 넘어갔고 생각했습니다.
그래서 Member 모듈도 trace로그를 찍어본 결과
게이트웨이의 호스트와 포트번호인 localhost:8080이 아닌 프론트의 호스트와 포트번호인 localhost:8081에서 요청이 들어왔다는 것을 확인할 수 있었습니다.
외부에서 들어온 요청을 받은 게이트웨이가 다시 요청을 보낼 것이라고 생각했었는데, 그게 아니라 게이트웨이는 들어온 요청에 헤더 등에 추가적인 정보를 추가해서 다른 모듈로 라우팅해주는 역할을 한다는 것을 알 수 있었습니다.
문제의 해결
그렇다면, Member, Live, Product, Order 등의 모듈에 프론트의 호스트와 포트 번호에 대한 CORS를 허용해주면 되는 것일까요?
그렇게 해도 문제를 해결할 수 있지만 정확한 해결 방법은 아닐 것입니다.
왜냐하면, SOP(Same Origin Policy)는 브라우저 사용자를 보호하기 위해서 브라우저 - 서버 간의 통신에서 적용되는 보안 정책이고
일반적으로 Server to Server 통신에서는 CORS에 대한 정책을 사용하지 않기 때문입니다.
CORS와 SOP에 대한 자세한 설명이 적힌 아티클이 있어, 일부를 캡처해 가져왔습니다.
그럼 어떻게 문제를 해결할 수 있을까요?
바로, Member, Live, Product, Order 모듈에서 CORS 설정을 삭제하는 것입니다.
CORS 설정으로 localhost:8080에 대한 요청을 열어두었기 때문에, 다른 origin에서 들어온 요청을 받을 수 없었습니다.
따라서 해당 설정을 없애주면 되는데, 간단히 Webconfig.java 파일을 삭제해주면 해결됩니다.
Reference
https://github.com/spring-cloud/spring-cloud-gateway/issues/2946
https://github.com/spring-cloud/spring-cloud-gateway/issues/3205
https://github.com/spring-cloud/spring-cloud-gateway/issues/3435