Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
fca75ad
MNMS-328 feat:gateway 디버깅
rookies-dogun Aug 12, 2025
76ba985
MNMS-328 fix: ìyaml 수정
rookies-dogun Aug 12, 2025
b1e69dc
MNMS-328 corfeat:s 해결
rookies-dogun Aug 12, 2025
be90f71
ãMNMS-328 feat: ã…gateway cors 해결
rookies-dogun Aug 12, 2025
25b34f0
MNMS-345 feat:claim 수정
rookies-dogun Aug 14, 2025
34686c0
MNMS-345 feat:claim 수정
rookies-dogun Aug 14, 2025
670e331
build(api-gateway): add production Dockerfile for Spring Boot (Java 17)
V4N1LLA Aug 19, 2025
f8fb72b
ci(api-gateway): add workflow api-gateway-docker-dispatch.yml (build …
V4N1LLA Aug 19, 2025
c738008
MNMS-346 feat: yaml 값 주석 달기 및 gateway webflux 변경
rookies-dogun Aug 20, 2025
d31ffb3
Merge branch 'develop' of https://github.com/3-mnms/gateway into develop
rookies-dogun Aug 20, 2025
6b0d830
ㅡMNMS-386 feat: Gateway Swagger Aggregator 추가
rookies-dogun Aug 20, 2025
1a40575
ㅡMNMS-386 feat: Gateway Swagger Aggregator 추가
rookies-dogun Aug 20, 2025
1132fdd
ㅡMNMS-386 feat: Gateway Swagger Aggregator ì develop/production 추가€
rookies-dogun Aug 20, 2025
949c0ec
ci(api): single dispatch with multi-targets (develop-gke,develop-aws)
V4N1LLA Aug 21, 2025
5069640
MNMS-328 feat:gateway cors 해결
rookies-dogun Aug 21, 2025
bfbea1a
Merge branch 'develop' of https://github.com/3-mnms/gateway into develop
rookies-dogun Aug 21, 2025
3b71324
MNMS-370 feat: FQDN 설정
rookies-dogun Aug 21, 2025
e098f26
MNMS-370 feat: update prod file
rookies-dogun Aug 21, 2025
29f695f
MNMS-370 feat: local 설정 추가
rookies-dogun Aug 21, 2025
b44b87b
MNMS-370 feat: predicates fix
rookies-dogun Aug 21, 2025
007523e
ci(workflows): 모든 서비스 멀티아키 이미지 푸시(amd64/arm64) + 태그 분리(-win/-mac)
V4N1LLA Aug 22, 2025
0887211
fix(dockerfile): Alpine → Jammy로 교체해 멀티아키(amd64/arm64) 빌드 실패 해결
V4N1LLA Aug 22, 2025
70c4cde
ci: 멀티아키 빌드 + latest(dev/main), v0.1 태그 추가
V4N1LLA Aug 23, 2025
252d34d
MNMS-504 feat: ãlcors 여려ê개 설정
rookies-dogun Aug 26, 2025
7c0ee9c
Merge branch 'develop' of https://github.com/3-mnms/gateway into develop
rookies-dogun Aug 26, 2025
747109a
MNMS-504 multi origin
rookies-dogun Aug 26, 2025
0462870
MNMS-512 feat: Gateway Upgrade
rookies-dogun Aug 27, 2025
6fafbe3
YAML 추ê가
rookies-dogun Sep 1, 2025
6441840
YAML 추ê가
rookies-dogun Sep 1, 2025
1357578
YAML 추ê가
rookies-dogun Sep 1, 2025
c60747e
push
rookies-dogun Sep 1, 2025
6cb7739
fix: 이름 깨짐 문제 수정
UN15 Sep 2, 2025
06a206c
yaml 값 추가
rookies-dogun Sep 2, 2025
01af780
MNMS-570 chore: prometheus 의존성 추가
V4N1LLA Sep 2, 2025
89747eb
Merge pull request #1 from 3-mnms/feature/MNMS-570
V4N1LLA Sep 2, 2025
59c0992
MNMS-468 ssl 설정
rookies-dogun Sep 4, 2025
db2db21
Merge branch 'develop' of https://github.com/3-mnms/gateway into develop
rookies-dogun Sep 4, 2025
618e851
feat: application-local.yml 수정(metric 활성화)
V4N1LLA Sep 4, 2025
345c7f1
feat: application-docker.yml 수정(metric 활성화)
V4N1LLA Sep 4, 2025
9192e0f
feat: build.gradle 의존성 추가
V4N1LLA Sep 4, 2025
7c908a6
MNMS-601 feat 수정
rookies-dogun Sep 5, 2025
cac9729
chore: application-docker.yml observation enabled 추가
V4N1LLA Sep 5, 2025
1b1c9ce
chore: build.gradle micrometer impl 추가
V4N1LLA Sep 5, 2025
da9e766
feat(gateway): routes 블록을 spring.cloud.gateway 루트로 이동하여 라우팅/메트릭 정상화
V4N1LLA Sep 5, 2025
b3a9d4a
cors 에러 해결
rookies-dogun Sep 10, 2025
d27bcd2
Merge branch 'develop' of https://github.com/3-mnms/gateway into develop
rookies-dogun Sep 10, 2025
3a4d64a
해결
rookies-dogun Sep 10, 2025
cd1d434
ws cors 해결
rookies-dogun Sep 10, 2025
ae58aee
payments 수정
rookies-dogun Sep 10, 2025
1f20ea8
header 두개 붙는 문제 해결
rookies-dogun Sep 10, 2025
bb09cc7
cors
rookies-dogun Sep 10, 2025
2805d7b
cors filter :
rookies-dogun Sep 10, 2025
7a559b8
feat: 만료 토큰 에러 처리 메시지 추가
UN15 Sep 11, 2025
1cb9393
fix: error 처리 방법 수정
UN15 Sep 15, 2025
46ad725
performance test
rookies-dogun Sep 16, 2025
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
93 changes: 93 additions & 0 deletions .github/workflows/api-gateway-docker-dispatch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: "💰 api-gateway – Docker Build & GitOps Dispatch"

on:
push:
branches: [ main, develop ]
workflow_dispatch: {}

permissions:
contents: read

concurrency:
group: api-gateway-${{ github.ref_name }}
cancel-in-progress: true

env:
SERVICE: gateway
IMAGE: ${{ secrets.DOCKER_USERNAME }}/api-gateway
DEVOPS_REPO: 3-mnms/gitops-repo

jobs:
build-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: '17'

- run: chmod +x ./gradlew
- run: ./gradlew bootJar --no-daemon

- name: ⏱️ Tag
run: echo "TAG=v$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_ENV

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=raw,value=${{ env.TAG }}
type=sha
type=raw,value=v0.1
type=raw,value=latest

- uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Dispatch to GitOps (develop-gke + develop-aws)
if: ${{ github.ref_name == 'develop' }}
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.GH_PAT }}
repository: ${{ env.DEVOPS_REPO }}
event-type: image-updated
client-payload: |
{
"service": "${{ env.SERVICE }}",
"image": "${{ env.IMAGE }}",
"tag": "${{ env.TAG }}",
"targets": "develop-gke,develop-aws"
}

- name: Dispatch to GitOps (prod)
if: ${{ github.ref_name == 'main' }}
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.GH_PAT }}
repository: ${{ env.DEVOPS_REPO }}
event-type: image-updated
client-payload: |
{
"service": "${{ env.SERVICE }}",
"image": "${{ env.IMAGE }}",
"tag": "${{ env.TAG }}",
"env": "prod"
}
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Debian/Ubuntu 계열 JRE (amd64/arm64 모두 제공)
FROM eclipse-temurin:17-jre-jammy

# spring 사용자/그룹 생성 (Debian 표준 명령)
RUN groupadd -r spring \
&& useradd -r -g spring -d /home/spring -s /usr/sbin/nologin spring \
&& mkdir -p /app /home/spring

WORKDIR /app

# 빌드 산출물 복사 (권한도 함께 설정)
ARG JAR_FILE=build/libs/*.jar
COPY --chown=spring:spring ${JAR_FILE} /app/app.jar

# 비루트 실행
USER spring

EXPOSE 8080
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]
75 changes: 42 additions & 33 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,59 +1,68 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.4'
id 'io.spring.dependency-management' version '1.1.7'
id 'java'
id 'org.springframework.boot' version '3.5.4'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.tekcit'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
toolchain { languageVersion = JavaLanguageVersion.of(17) }
}

repositories {
mavenCentral()
mavenCentral()
}

ext {
set('springCloudVersion', "2025.0.0")
set('springCloudVersion', "2025.0.0")
}

dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-kubernetes-client-all'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-security'
// Core
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'

// ★ Spring Cloud Gateway (starter 하나면 충분; WebFlux 포함됨)
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'

// Kubernetes (필요시)
implementation 'org.springframework.cloud:spring-cloud-starter-kubernetes-client-all'

// JWT / Security
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.springframework.security:spring-security-oauth2-jose'
implementation 'org.springframework.security:spring-security-oauth2-resource-server'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.springframework.security:spring-security-oauth2-jose'
implementation 'org.springframework.security:spring-security-oauth2-resource-server'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// JSON
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'org.springframework.boot:spring-boot-starter-json'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.8.9'

// ★ Prometheus 레지스트리
implementation 'io.micrometer:micrometer-registry-prometheus'

// JSON 직렬화용
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'org.springframework.boot:spring-boot-starter-json'
testImplementation 'org.springframework.kafka:spring-kafka-test'
// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.springframework.kafka:spring-kafka-test'


}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

tasks.named('test') { useJUnitPlatform() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.tekcit.gateway.config.gateway.filter;

import com.tekcit.gateway.config.gateway.property.CorsProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class GatewayCorsConfig {


private final CorsProperties corsProperties;


// @Bean
// public CorsWebFilter corsWebFilter() {
// CorsConfiguration config = new CorsConfiguration();
// for(String url : corsProperties.getUrl()){
// config.addAllowedOrigin(url);
// }
// config.addAllowedMethod("*");
// config.addAllowedHeader("*");
// config.setAllowCredentials(true);
//
// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//
// source.registerCorsConfiguration("/**", config);
//
// return new CorsWebFilter(source);
// }
//

@Bean
public WebFilter corsFilter() {
return (exchange, chain) -> {
String path = exchange.getRequest().getURI().getPath();
if (path.startsWith("/ws")) {
// WebSocket 경로는 CORS 필터 건너뛰기 (서버로 그대로 전달)
return chain.filter(exchange);
}

CorsConfiguration config = new CorsConfiguration();
for (String url : corsProperties.getUrl()) {
config.addAllowedOrigin(url);
}
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

return new CorsWebFilter(source).filter(exchange, chain);
};
}


}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tekcit.gateway.config.gateway.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tekcit.gateway.config.security.jwt.JwtTokenProvider;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
Expand All @@ -22,6 +23,35 @@ public class JwtToHeaderFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

String upgradeHeader = exchange.getRequest().getHeaders().getFirst("Upgrade");
System.out.println(upgradeHeader);


if (upgradeHeader != null && upgradeHeader.equalsIgnoreCase("websocket")) {
// WebSocket handshake 요청이면 SecurityContext와 상관없이 JWT를 직접 읽어 헤더 추가
String token = exchange.getRequest().getQueryParams().getFirst("token");
exchange.getRequest().getHeaders().forEach((key, values) -> {
System.out.println(key + " : " + values);
});
if (token != null && token.startsWith("Bearer ")) {
String jwt = token.substring(7);
Claims claims = jwtTokenProvider.getAllClaims(jwt);
System.out.println(" ============= WebSocket Authorization ================");
System.out.println("X-User-Id" + jwtTokenProvider.getSubject(claims));
System.out.println("X-User-Name" + jwtTokenProvider.getName(claims));
System.out.println("X-User-Role" + jwtTokenProvider.getClaimAsString(claims, "role"));
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.headers(h -> {
h.set("X-User-Id", jwtTokenProvider.getSubject(claims));
h.set("X-User-Name", jwtTokenProvider.getName(claims));
h.set("X-User-Role", jwtTokenProvider.getClaimAsString(claims, "role"));
})
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
}

return ReactiveSecurityContextHolder.getContext()
.doOnNext(ctx -> System.out.println("SecurityContext 있음: " + ctx))
.map(securityContext -> securityContext.getAuthentication())
Expand All @@ -31,12 +61,24 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
.flatMap(auth -> {
String jwt = auth.getToken().getTokenValue();
Claims claims = jwtTokenProvider.getAllClaims(jwt);

// 요청 주소 확인
ServerHttpRequest request = exchange.getRequest();
System.out.println("요청 URI: " + request.getURI());
System.out.println("요청 Path: " + request.getPath());
System.out.println("요청 Method: " + request.getMethod());

System.out.println("X-User-Id " + jwtTokenProvider.getSubject(claims));
System.out.println("X-User-Name " + jwtTokenProvider.getName(claims));
System.out.println("X-User-Role " + jwtTokenProvider.getClaimAsString(claims, "role"));

ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Id", jwtTokenProvider.getClaimAsString(claims, "userId"))
.header("X-User-Name", jwtTokenProvider.getClaimAsString(claims,"name"))
.header("X-User-Role", jwtTokenProvider.getClaimAsString(claims,"role"))
.headers(h ->{
h.set("X-User-Id", jwtTokenProvider.getSubject(claims));
h.set("X-User-Name", jwtTokenProvider.getName(claims));
h.set("X-User-Role", jwtTokenProvider.getClaimAsString(claims, "role"));
})
.build();

return chain.filter(exchange.mutate().request(mutatedRequest).build());
})
.switchIfEmpty(Mono.defer(() -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.tekcit.gateway.config.gateway.property;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@Getter @Setter
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
private List<String> url;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public PasswordEncoder passwordEncoder() {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.cors(ServerHttpSecurity.CorsSpec::disable)
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(auth -> auth
.anyExchange().permitAll()
Expand Down
Loading