[Feature] Gateway 구성 - #42
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough새로운 Spring Cloud Gateway 모듈 Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as Client
participant Gateway as API Gateway\n(gateway-server:8000)
participant Auth as OAuth2\nIssuer
participant Eureka as Eureka\nRegistry
participant Service as Downstream\nMicroservice
Client->>Gateway: HTTP Request (Authorization: Bearer <JWT>)
Gateway->>Auth: Validate JWT (issuer-uri)
Auth-->>Gateway: JWT Valid + Claims
Gateway->>Gateway: UserHeaderFilter\nExtract/Inject X-User-Id & X-User-Role
Gateway->>Eureka: Resolve lb://SERVICE
Eureka-->>Gateway: Service Instance Info
Gateway->>Service: Forward Request + X-User-* headers
Service-->>Gateway: Response
Gateway-->>Client: HTTP Response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsTimed out fetching pipeline failures after 30000ms Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (4)
user-service/src/main/java/com/shipflow/userservice/presentation/controller/AuthController.java (1)
51-51:System.out.println디버그 출력은 제거하거나 기존 로거로 통일해주세요.이미
@Slf4j를 쓰고 있어서 이 한 줄만 stdout으로 남기면 운영 환경에서 로그 레벨 제어와 필터링이 깨집니다. 로그인 엔드포인트라 호출량이 많아질수록 더 noisy 합니다.수정 예시
- System.out.println("[UserService] login endpoint called"); + log.debug("[UserService] login endpoint called");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@user-service/src/main/java/com/shipflow/userservice/presentation/controller/AuthController.java` at line 51, Replace the raw System.out.println call in AuthController (the line printing "[UserService] login endpoint called") with the class logger provided by `@Slf4j` (e.g., use log.info or log.debug depending on desired verbosity) so logs are centralized and respect log levels; remove the println and use log at the appropriate level in the login handling method to keep logging consistent and controllable in production.user-service/build.gradle (1)
37-37: Eureka client 의존성이 중복 선언되어 있습니다.Line 37 추가분이 Line 41 기존 선언과 동일합니다. 빌드는 되더라도 의존성 목록만 헷갈리게 하니 하나만 남겨두는 편이 좋습니다.
정리 예시
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' runtimeOnly 'org.postgresql:postgresql' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'Also applies to: 41-41
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@user-service/build.gradle` at line 37, The dependency 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' is declared twice (the implementation line at startLine 37 duplicates the existing declaration); remove one of the duplicate implementation entries so the dependency appears only once in build.gradle (locate the implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' lines and delete the redundant one).gateway-server/src/main/resources/application.yaml (1)
52-52: 환경별 설정 외부화 필요
issuer-uri와defaultZone이localhost로 하드코딩되어 있어 Docker/Kubernetes 환경에서 동작하지 않습니다. 환경 변수나 Spring Profile로 외부화하세요.♻️ 환경 변수 사용 제안
security: oauth2: resourceserver: jwt: - issuer-uri: http://localhost:9001/realms/shipflow + issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:9001/realms/shipflow} eureka: client: register-with-eureka: true fetch-registry: true service-url: - defaultZone: http://localhost:8761/eureka/ + defaultZone: ${EUREKA_SERVER_URL:http://localhost:8761/eureka/}Also applies to: 59-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gateway-server/src/main/resources/application.yaml` at line 52, The YAML currently hardcodes issuer-uri and defaultZone to localhost; change both to use externalized properties (e.g., Spring placeholders) and reference environment variables or profiles instead of literal values — replace the literal issuer-uri and defaultZone entries with property placeholders like ${OAUTH_ISSUER_URI:...} and ${EUREKA_DEFAULT_ZONE:...} (or bind them to profile-specific files), and ensure the application reads these properties (e.g., via Spring `@Value` or configuration properties) so Docker/Kubernetes can inject values at runtime; update any code relying on these keys accordingly (look for issuer-uri and defaultZone occurrences in the config).gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java (1)
53-64: 여러 역할을 가진 사용자의 경우X-User-Role매핑 정책을 명확히 하세요.Line 63에서 첫 번째 역할만 추출하면, JWT의 역할 순서에 따라 권한 판정이 예측 불가능하게 변할 수 있습니다. 다운스트림 서비스들이 모두 단일 역할값을 기대하므로, 다음 중 하나를 명시해야 합니다:
- 사용자는 정확히 하나의 애플리케이션 역할만 가져야 한다는 정책 문서화
- 역할 우선순위 규칙 구현 (예: MASTER > HUB_MANAGER > SHIPMENT_MANAGER > COMPANY_MANAGER)
- JWT의 역할이 정확히 하나임을 검증하는 로직 추가
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java` around lines 53 - 64, The extractRole(Jwt) currently returns the first role arbitrarily; change it to handle multiple roles deterministically: retrieve realm_access.roles from the Jwt in extractRole (UserHeaderFilter), validate presence as now, then if roles.size() == 1 return that role, otherwise apply a deterministic priority ordering (e.g., MASTER > HUB_MANAGER > SHIPMENT_MANAGER > COMPANY_MANAGER) to pick and return the highest-priority role; if you prefer strict validation instead, throw a new BusinessException (add a GateErrorCode like MULTIPLE_ROLES) when roles.size() != 1. Ensure the chosen approach is implemented in extractRole and use the same BusinessException/GateErrorCode pattern already used for missing claims.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docker-compose.yml`:
- Around line 74-75: The docker-compose service currently uses a short host port
mapping under the ports key (ports: - "8000") which publishes the container port
to a random host port; update the ports mapping for the gateway service to
explicitly bind host port 8000 to container port 8000 (replace the existing
"8000" entry with an explicit "8000:8000" mapping) so the gateway is reachable
at localhost:8000.
In `@gateway-server/build.gradle`:
- Around line 21-23: The Spring Cloud BOM version in the gradle ext property
"springCloudVersion" is inconsistent across services (gateway-server uses
"2025.0.2" while product-service and discovery-server use "2025.0.0"); update
the ext.set('springCloudVersion', ...) value in gateway-server to match the
chosen project-wide Spring Cloud version (or update the other services to
2025.0.2) so all modules use the same BOM version, ensuring the ext block and
the set('springCloudVersion', ...) symbol are changed consistently across
services.
In
`@gateway-server/src/main/java/com/shipflow/gatewayserver/config/SecurityConfig.java`:
- Line 26: 상수 HUBSROUTES 값이 "/api/hubs-routes/**"로 오타가 있어 실제 보안 규칙(antMatchers
블록, 70-72행에 있는 관리자 권한 검사)과 불일치하므로 해당 값을 "/api/hub-routes/**"로 수정하여
SecurityConfig의 HUBSROUTES 상수와 configure/antMatchers(admin) 규칙이 일치하도록 변경하세요; 변경
대상 식별자: HUBSROUTES 및 security 규칙이 정의된 configure(HttpSecurity) 또는
antMatchers(...) 블록(70-72 행 참조).
- Around line 35-84: Add a global reactive exception handler that converts
BusinessException thrown by UserHeaderFilter.extractRole() into proper HTTP
responses: implement an ErrorWebExceptionHandler bean that catches
BusinessException, uses the exception's GateErrorCode.status() to set the
ServerHttpResponse status code, writes an appropriate error body (JSON with
code/message) to the response in a non-blocking way, and returns a Mono<Void>;
ensure other exceptions still fall through to default handling. Reference:
UserHeaderFilter.extractRole(), BusinessException, GateErrorCode.status(), and
ErrorWebExceptionHandler.
- Around line 16-20: WHITELIST currently contains "/api/auth/signup-requests/**"
which causes .pathMatchers(WHITELIST).permitAll() to bypass subsequent role
checks; remove "/api/auth/signup-requests/**" from the WHITELIST constant and
instead explicitly permit only POST for signup requests by adding a dedicated
matcher that allows HttpMethod.POST to "/api/auth/signup-requests" (or
"/api/auth/signup-requests/**" if necessary) while keeping PATCH/GET protected
by the existing .hasAnyRole("MASTER","HUB_MANAGER") checks so PATCH/GET are no
longer globally permitted.
In
`@gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java`:
- Around line 27-29: exchange.getPrincipal().cast(Authentication.class)로 강제
캐스팅하면 잘못된 principal 타입에서 ClassCastException이 발생할 수 있으니
cast(Authentication.class)을 ofType(Authentication.class)로 바꿔서 타입이 일치하지 않을 경우 빈
스트림을 반환하도록 변경하세요; 이렇게 하면 이후의 switchIfEmpty(...) 흐름(예: 인증 없을 때 처리)을 안전하게 타게 됩니다.
In `@gateway-server/src/main/resources/application.yaml`:
- Around line 18-26: The company route (id: company, Path=/api/companies/**) is
declared before the more specific product route (id: product,
Path=/api/companies/*/products/**) which can cause product requests to be
matched by the company route; reorder the route definitions in application.yaml
so the product route block (id: product with Path=/api/companies/*/products/**)
appears before the company route block (id: company with Path=/api/companies/**)
to ensure the more specific route is evaluated first by Spring Cloud Gateway.
- Around line 28-31: Add the Eureka client dependency to the order-service build
configuration so the service registers with Eureka and the gateway's
lb://ORDERSERVICE route works; update the order-service build.gradle to include
the spring-cloud-starter-netflix-eureka-client dependency (and ensure the
service application class or configuration enables Eureka client registration if
not already present, e.g., referenced from the order-service application
starter).
In
`@gateway-server/src/test/java/com/shipflow/gatewayserver/GatewayServerApplicationTests.java`:
- Around line 6-11: The test class GatewayServerApplicationTests currently uses
`@SpringBootTest` which loads production autoconfiguration and can try to contact
Eureka and Keycloak; fix by preventing external service wiring—either add a test
profile (e.g., annotate GatewayServerApplicationTests with
`@ActiveProfiles`("test") and add src/test/resources/application-test.yaml with
eureka.client.enabled: false and an empty
spring.security.oauth2.resourceserver.jwt.issuer-uri) or override properties on
the test class (e.g., add properties to the `@SpringBootTest` annotation to set
eureka.client.enabled=false and clear the issuer-uri); update the test class
annotation or add the test YAML and keep the contextLoads() test as-is.
In `@user-service/src/main/resources/application.yaml`:
- Around line 19-23: The JwtDecoder bean in SecurityConfig.java is hardcoded to
"http://localhost:9001/realms/shipflow" so it ignores application.yaml and fails
in Docker; change the JwtDecoder bean (method jwtDecoder) to accept the issuer
URI from configuration (inject
spring.security.oauth2.resourceserver.jwt.issuer-uri, e.g. via `@Value` or
Environment) and instantiate the decoder with
JwtDecoders.fromIssuerLocation(issuerUri) instead of the hardcoded string, and
ensure your environment-specific configs set
spring.security.oauth2.resourceserver.jwt.issuer-uri to
http://localhost:9001/realms/shipflow for local and
http://keycloak:8080/realms/shipflow for Docker Compose.
---
Nitpick comments:
In
`@gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java`:
- Around line 53-64: The extractRole(Jwt) currently returns the first role
arbitrarily; change it to handle multiple roles deterministically: retrieve
realm_access.roles from the Jwt in extractRole (UserHeaderFilter), validate
presence as now, then if roles.size() == 1 return that role, otherwise apply a
deterministic priority ordering (e.g., MASTER > HUB_MANAGER > SHIPMENT_MANAGER >
COMPANY_MANAGER) to pick and return the highest-priority role; if you prefer
strict validation instead, throw a new BusinessException (add a GateErrorCode
like MULTIPLE_ROLES) when roles.size() != 1. Ensure the chosen approach is
implemented in extractRole and use the same BusinessException/GateErrorCode
pattern already used for missing claims.
In `@gateway-server/src/main/resources/application.yaml`:
- Line 52: The YAML currently hardcodes issuer-uri and defaultZone to localhost;
change both to use externalized properties (e.g., Spring placeholders) and
reference environment variables or profiles instead of literal values — replace
the literal issuer-uri and defaultZone entries with property placeholders like
${OAUTH_ISSUER_URI:...} and ${EUREKA_DEFAULT_ZONE:...} (or bind them to
profile-specific files), and ensure the application reads these properties
(e.g., via Spring `@Value` or configuration properties) so Docker/Kubernetes can
inject values at runtime; update any code relying on these keys accordingly
(look for issuer-uri and defaultZone occurrences in the config).
In `@user-service/build.gradle`:
- Line 37: The dependency
'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' is
declared twice (the implementation line at startLine 37 duplicates the existing
declaration); remove one of the duplicate implementation entries so the
dependency appears only once in build.gradle (locate the implementation
'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' lines and
delete the redundant one).
In
`@user-service/src/main/java/com/shipflow/userservice/presentation/controller/AuthController.java`:
- Line 51: Replace the raw System.out.println call in AuthController (the line
printing "[UserService] login endpoint called") with the class logger provided
by `@Slf4j` (e.g., use log.info or log.debug depending on desired verbosity) so
logs are centralized and respect log levels; remove the println and use log at
the appropriate level in the login handling method to keep logging consistent
and controllable in production.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 435aa81d-191f-43e9-b29c-884506b27037
⛔ Files ignored due to path filters (1)
gateway-server/gradle/wrapper/gradle-wrapper.jaris excluded by!**/*.jar
📒 Files selected for processing (19)
docker-compose.ymlgateway-server/.gitattributesgateway-server/.gitignoregateway-server/build.gradlegateway-server/gradle/wrapper/gradle-wrapper.propertiesgateway-server/gradlewgateway-server/gradlew.batgateway-server/src/main/java/com/shipflow/gatewayserver/GatewayServerApplication.javagateway-server/src/main/java/com/shipflow/gatewayserver/config/SecurityConfig.javagateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.javagateway-server/src/main/java/com/shipflow/gatewayserver/exception/BusinessException.javagateway-server/src/main/java/com/shipflow/gatewayserver/exception/GateErrorCode.javagateway-server/src/main/resources/application.yamlgateway-server/src/test/java/com/shipflow/gatewayserver/GatewayServerApplicationTests.javasettings.gradleuser-service/build.gradleuser-service/src/main/java/com/shipflow/userservice/UserserviceApplication.javauser-service/src/main/java/com/shipflow/userservice/presentation/controller/AuthController.javauser-service/src/main/resources/application.yaml
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java (1)
59-63: 다중 역할 토큰에서roles.get(0)고정 선택은 비결정적일 수 있습니다.Line 63은 첫 번째 역할만 전달하므로 역할 배열 순서에 따라 다운스트림 동작이 달라질 수 있습니다. 우선순위 정책(예:
MASTER > HUB_MANAGER > ...)을 명시하거나, 다중 역할 표현 포맷을 정의해 전달하는 방식이 더 안전합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java` around lines 59 - 63, UserHeaderFilter currently picks roles.get(0) from realmAccess.get("roles") which makes role selection order-dependent; update the logic in UserHeaderFilter to handle multiple roles deterministically by defining a priority list (e.g., ["MASTER","HUB_MANAGER",...]) and selecting the highest-priority role present in the roles list (instead of roles.get(0)), or alternatively serialize the entire roles collection into the header if downstream expects multi-role format; ensure you still throw BusinessException(GateErrorCode.MISSING_ROLES) when roles is missing/empty and reference the realmAccess.get("roles") retrieval and roles variable when implementing the prioritized selection or serialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@gateway-server/src/main/java/com/shipflow/gatewayserver/config/SecurityConfig.java`:
- Around line 42-44: The security config is using jwt(Customizer.withDefaults())
which doesn't map roles from the token's realm_access.roles into Spring
authorities, causing 403s; update SecurityConfig to supply a custom
JwtAuthenticationConverter (used in the
HttpSecurity.oauth2ResourceServer().jwt(...)) that reads the
"realm_access.roles" claim, converts each role into a GrantedAuthority with
"ROLE_" prefix (e.g., "MASTER" -> "ROLE_MASTER"), and set that converter via
jwtAuthenticationConverter on the JwtAuthenticationProvider so
hasRole/hasAnyRole checks (e.g., in the pathMatchers rules) succeed; ensure this
converter logic is implemented as a reusable method/class name (e.g.,
createRealmAccessJwtConverter or RealmAccessJwtGrantedAuthoritiesConverter) and
wired into the security filter chain instead of Customizer.withDefaults().
In
`@gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java`:
- Around line 36-50: The filter currently only removes USER_ID_HEADER and
USER_ROLE_HEADER inside the JWT-authenticated branch, allowing client-supplied
X-User-* headers to slip through on the non-JWT/principal path; update
UserHeaderFilter so the request headers are stripped unconditionally before
authentication logic by mutating exchange.getRequest() to remove USER_ID_HEADER
and USER_ROLE_HEADER (refer to USER_ID_HEADER, USER_ROLE_HEADER and the code
that builds "mutated" and calls
chain.filter(exchange.mutate().request(mutated).build() ) ), then continue with
existing JWT principal checks and re-add trusted headers only when available.
---
Nitpick comments:
In
`@gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java`:
- Around line 59-63: UserHeaderFilter currently picks roles.get(0) from
realmAccess.get("roles") which makes role selection order-dependent; update the
logic in UserHeaderFilter to handle multiple roles deterministically by defining
a priority list (e.g., ["MASTER","HUB_MANAGER",...]) and selecting the
highest-priority role present in the roles list (instead of roles.get(0)), or
alternatively serialize the entire roles collection into the header if
downstream expects multi-role format; ensure you still throw
BusinessException(GateErrorCode.MISSING_ROLES) when roles is missing/empty and
reference the realmAccess.get("roles") retrieval and roles variable when
implementing the prioritized selection or serialization.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 09f6b714-37ef-4f1d-96c3-210324f78268
📒 Files selected for processing (5)
docker-compose.ymlgateway-server/build.gradlegateway-server/src/main/java/com/shipflow/gatewayserver/config/SecurityConfig.javagateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.javagateway-server/src/main/resources/application.yaml
✅ Files skipped from review due to trivial changes (2)
- gateway-server/src/main/resources/application.yaml
- docker-compose.yml
🚧 Files skipped from review as they are similar to previous changes (1)
- gateway-server/build.gradle
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java`:
- Around line 45-54: The code in UserHeaderFilter uses
jwt.getClaimAsString("userId") and ("role") and may add null values to headers;
update the logic in the filter (around jwt.getClaimAsString usage and the
request mutation that builds 'mutated' from 'sanitizedExchange') to validate
these claims first: if either userId or role is null/empty, reject the request
(send an error response/deny) rather than adding headers, otherwise add only the
non-null values to headers (USER_ID_HEADER, USER_ROLE_HEADER) when building the
mutated ServerHttpRequest; ensure the decision point is colocated with the
current mutate() call so downstream never receives null header values.
- Around line 67-78: The extractRole(Jwt) method is dead code and conflicts with
the current claim access (jwt.getClaimAsString("role")); decide which JWT shape
is correct and act accordingly: if the token uses a flat "role" claim, remove
extractRole(Jwt) and the unused GateErrorCode.MISSING_REALM_ACCESS /
MISSING_ROLES enums/usages; if the token follows Keycloak's realm_access.roles
structure, replace jwt.getClaimAsString("role") in the filter with
extractRole(jwt) and ensure the BusinessException codes
(GateErrorCode.MISSING_REALM_ACCESS and GateErrorCode.MISSING_ROLES) remain
available; verify the actual JWT claim structure while making the change so
extraction matches the token format.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7f931a47-3b65-444f-9a61-c9cfae2dc1cf
📒 Files selected for processing (2)
gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.javaorder-service/build.gradle
✅ Files skipped from review due to trivial changes (1)
- order-service/build.gradle
📌 PR 제목
[Feature] Gateway 구성
✨ 작업 내용
게이트웨이 구성하고 기존 eureka, docker환경에 추가하였습니다.
🔍 상세 내용
🔗 관련 이슈
Closes #2
다른 도메인에서 문제있을 시 디엠주시면 바로 확인하겠습니다.
✅ 체크리스트
Summary by CodeRabbit
새로운 기능
동작 변경 / 구성
테스트