diff --git a/.github/workflows/api-gateway-docker-dispatch.yml b/.github/workflows/api-gateway-docker-dispatch.yml new file mode 100644 index 0000000..4d728ab --- /dev/null +++ b/.github/workflows/api-gateway-docker-dispatch.yml @@ -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" + } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..838322d --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index fb4262f..15c364b 100644 --- a/build.gradle +++ b/build.gradle @@ -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() } \ No newline at end of file diff --git a/src/main/java/com/tekcit/gateway/config/gateway/filter/GatewayCorsConfig.java b/src/main/java/com/tekcit/gateway/config/gateway/filter/GatewayCorsConfig.java new file mode 100644 index 0000000..e083ff5 --- /dev/null +++ b/src/main/java/com/tekcit/gateway/config/gateway/filter/GatewayCorsConfig.java @@ -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); + }; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/tekcit/gateway/config/gateway/filter/JwtToHeaderFilter.java b/src/main/java/com/tekcit/gateway/config/gateway/filter/JwtToHeaderFilter.java index fa6a2bb..f3460c5 100644 --- a/src/main/java/com/tekcit/gateway/config/gateway/filter/JwtToHeaderFilter.java +++ b/src/main/java/com/tekcit/gateway/config/gateway/filter/JwtToHeaderFilter.java @@ -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; @@ -22,6 +23,35 @@ public class JwtToHeaderFilter implements GlobalFilter, Ordered { @Override public Mono 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()) @@ -31,12 +61,24 @@ public Mono 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(() -> { diff --git a/src/main/java/com/tekcit/gateway/config/gateway/property/CorsProperties.java b/src/main/java/com/tekcit/gateway/config/gateway/property/CorsProperties.java new file mode 100644 index 0000000..a08e40c --- /dev/null +++ b/src/main/java/com/tekcit/gateway/config/gateway/property/CorsProperties.java @@ -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 url; +} diff --git a/src/main/java/com/tekcit/gateway/config/security/WebfluxSecurityConfig.java b/src/main/java/com/tekcit/gateway/config/security/WebfluxSecurityConfig.java index b4d976d..f72869f 100644 --- a/src/main/java/com/tekcit/gateway/config/security/WebfluxSecurityConfig.java +++ b/src/main/java/com/tekcit/gateway/config/security/WebfluxSecurityConfig.java @@ -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() diff --git a/src/main/java/com/tekcit/gateway/config/security/jwt/JwtTokenProvider.java b/src/main/java/com/tekcit/gateway/config/security/jwt/JwtTokenProvider.java index 60137a9..8e1c19b 100644 --- a/src/main/java/com/tekcit/gateway/config/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/tekcit/gateway/config/security/jwt/JwtTokenProvider.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; @@ -82,16 +83,19 @@ public Jwt convertToSpringJwt(String token) { Map claimsMap = new HashMap<>(claims); - Integer expInt = (Integer) claims.get("exp"); - Integer iatInt = (Integer) claims.get("iat"); + Object expObj = claims.get("exp"); + Object iatObj = claims.get("iat"); - Instant expiresAt = Instant.ofEpochSecond(expInt.longValue()); - Instant issuedAt = Instant.ofEpochSecond(iatInt.longValue()); + Long exp = expObj instanceof Number ? ((Number) expObj).longValue() : null; + Long iat = iatObj instanceof Number ? ((Number) iatObj).longValue() : null; + + Instant expiresAt = Instant.ofEpochSecond(exp); + Instant issuedAt = Instant.ofEpochSecond(iat); return new Jwt(token, issuedAt, expiresAt, headers, claimsMap); } - // Claim ์ถ”์ถœ + // ํ† ํฐ ์ถ”์ถœ public Claims getAllClaims(String token) { return Jwts.parserBuilder() .setSigningKey(publicKey) @@ -100,6 +104,13 @@ public Claims getAllClaims(String token) { .parseClaimsJws(token) .getBody(); } + public String getSubject(String token){ + return getAllClaims(token).getSubject(); + } + public String getSubject(Claims claims){ + return claims.getSubject(); + } + public String getClaimAsString(Claims claims, String claimName){ return claims.get(claimName).toString(); } @@ -120,7 +131,9 @@ public String getName(String token){ } public String getName(Claims claims){ - return claims.get("name", String.class); + String raw = claims.get("name", String.class); + String name = Base64.getUrlEncoder().encodeToString(raw.getBytes(StandardCharsets.UTF_8)); + return name; } // ===== PEM ๋กœ๋”๋“ค diff --git a/src/main/java/com/tekcit/gateway/config/security/repository/JwtSecurityContextRepository.java b/src/main/java/com/tekcit/gateway/config/security/repository/JwtSecurityContextRepository.java index a81b223..adbd959 100644 --- a/src/main/java/com/tekcit/gateway/config/security/repository/JwtSecurityContextRepository.java +++ b/src/main/java/com/tekcit/gateway/config/security/repository/JwtSecurityContextRepository.java @@ -1,7 +1,10 @@ package com.tekcit.gateway.config.security.repository; import com.tekcit.gateway.config.security.jwt.JwtTokenProvider; +import com.tekcit.gateway.exception.ApiErrorUtils; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -45,8 +48,15 @@ public Mono load(ServerWebExchange exchange) { JwtAuthenticationToken auth = new JwtAuthenticationToken(jwtTokenProvider.convertToSpringJwt(token), authorities); return Mono.just(new SecurityContextImpl(auth)); + } catch (ExpiredJwtException e) { + return ApiErrorUtils.unauthorized(exchange, "AUTH_ACCESS_TOKEN_EXPIRED", "์•ก์„ธ์Šค ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + .then(Mono.empty()); + } catch (JwtException e) { + return ApiErrorUtils.unauthorized(exchange, "AUTH_INVALID_TOKEN", "์œ ํšจํ•˜์ง€ ์•Š์€ ์•ก์„ธ์Šค ํ† ํฐ์ž…๋‹ˆ๋‹ค.") + .then(Mono.empty()); } catch (Exception e) { - return Mono.empty(); + return ApiErrorUtils.unauthorized(exchange, "AUTHENTICATION_ERROR", "ํ† ํฐ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.") + .then(Mono.empty()); } } } \ No newline at end of file diff --git a/src/main/java/com/tekcit/gateway/config/swagger/OpenApiGatewayConfig.java b/src/main/java/com/tekcit/gateway/config/swagger/OpenApiGatewayConfig.java new file mode 100644 index 0000000..76992c6 --- /dev/null +++ b/src/main/java/com/tekcit/gateway/config/swagger/OpenApiGatewayConfig.java @@ -0,0 +1,18 @@ +package com.tekcit.gateway.config.swagger; + + +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiGatewayConfig { + + @Bean + public GroupedOpenApi apiGroup(){ + return GroupedOpenApi.builder() + .group("all-services") + .pathsToMatch("/**") + .build(); + } +} diff --git a/src/main/java/com/tekcit/gateway/exception/ApiErrorUtils.java b/src/main/java/com/tekcit/gateway/exception/ApiErrorUtils.java new file mode 100644 index 0000000..04ba8b7 --- /dev/null +++ b/src/main/java/com/tekcit/gateway/exception/ApiErrorUtils.java @@ -0,0 +1,49 @@ +package com.tekcit.gateway.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +public final class ApiErrorUtils { + + private ApiErrorUtils() {} + + public static Mono write(ServerWebExchange exchange, HttpStatus status, String code, String message) { + if (exchange.getResponse().isCommitted()) return Mono.empty(); + + exchange.getResponse().setStatusCode(status); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + + String body = String.format( + "{\"success\":false," + + "\"errorCode\":\"%s\"," + + "\"errorMessage\":\"%s\"}", + code, escape(message) + ); + + var buf = exchange.getResponse() + .bufferFactory() + .wrap(body.getBytes(StandardCharsets.UTF_8)); + + return exchange.getResponse().writeWith(Mono.just(buf)); + } + + public static Mono unauthorized(ServerWebExchange ex, String code, String msg) { + return write(ex, HttpStatus.UNAUTHORIZED, code, msg); + } + + public static Mono forbidden(ServerWebExchange ex, String code, String msg) { + return write(ex, HttpStatus.FORBIDDEN, code, msg); + } + + public static Mono badRequest(ServerWebExchange ex, String code, String msg) { + return write(ex, HttpStatus.BAD_REQUEST, code, msg); + } + + private static String escape(String s) { + return s == null ? "" : s.replace("\"", "\\\""); + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml deleted file mode 100644 index 06e5df1..0000000 --- a/src/main/resources/application-dev.yml +++ /dev/null @@ -1,56 +0,0 @@ -server: - port: 8080 - -spring: - application: - name: api-gateway - cloud: - kubernetes: - discovery: - enabled: true - namespaces: - - booking - - festival - - payment - - user - service-labels: - expose-via-spring-gateway: "true" - gateway: - default-filters: - - StripPrefix=1 - discovery: - locator: - enabled: true - lower-case-service-id: true - predicates: - - name: Path - args: - pattern: "'/' + serviceId + '/**'" - filters: - - name: StripPrefix - args: - parts: 1 - -jwt: - private-pem-path: classpath:keys/private.pem - public-pem-path: classpath:keys/public.pem - - - -management: - endpoints: - web: - exposure: - include: '*' - health: - show-details: always - endpoint: - gateway: - access: unrestricted - -logging: - level: - org.springframework.cloud.gateway: DEBUG - org.springframework.cloud.gateway.discovery: DEBUG - org.springframework.cloud.kubernetes.discovery: DEBUG - reactor.netty.http.client.HttpClient: DEBUG \ No newline at end of file diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml new file mode 100644 index 0000000..92ab792 --- /dev/null +++ b/src/main/resources/application-develop.yml @@ -0,0 +1,128 @@ +server: + port: 8080 + +spring: + application: + name: api-gateway + cloud: + # Kubernetes ์ž๋™ Routing ์„ค์ • ๋ฐ ๊ฒ€์ƒ‰ ๋ฒ”์œ„ namespace ์„ค์ • + kubernetes: + discovery: + enabled: true + namespaces: + - booking + - festival + - payment + - user + # ๊ฒ€์ƒ‰ ์กฐ๊ฑด : service ๊ฐ€ ์ด ๋ผ๋ฒจ์„ ํฌํ•จํ•  ๊ฒฝ์šฐ + service-labels: + expose-via-spring-gateway: "true" + gateway: + server: + webflux: + routes: + - id: user + uri: http://api-user-service.user.svc.cluster.local:8080 + predicates: + - Path=/api/user/**,/api/addresses/**,/api/admin/**,/api/users/**,/api/mail/**,/api/auth/**,/api/myPage/** + - id: user-swagger + uri: http://api-user-service.user.svc.cluster.local:8080 + predicates: + - Path=/api/user/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + + - id: payment + uri: http://api-payment-service.payment.svc.cluster.local:8080 + predicates: + - Path=/api/payments/**,/api/tekcitpay/** + + - id: payment-swagger + uri: http://api-payment-service.payment.svc.cluster.local:8080 + predicates: + - Path=/api/payments/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + + - id: booking + uri: http://api-booking-service.booking.svc.cluster.local:8080 + predicates: + - Path=/api/booking/**,/api/qr/**,/api/host/**,/api/captcha/**,/api/transfer/**,/api/ticket/**,/api/statistics/**,/ws/** + - id: booking_ws_route + uri: ws://api-booking-service.booking.svc.cluster.local:8080 + predicates: + - Path=/ws/** + - id: booking-swagger + uri: http://api-booking-service.booking.svc.cluster.local:8080 + predicates: + - Path=/api/booking/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + - id: festival + uri: http://api-festival-service.festival.svc.cluster.local:8080 + predicates: + - Path=/api/festival/**,/uploads/** + + - id: festival-swagger + uri: http://api-festival-service.festival.svc.cluster.local:8080 + predicates: + - Path=/api/festival/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + +# JWT key ์„ธํŒ… +jwt: + private-pem-path: classpath:keys/private.pem + public-pem-path: classpath:keys/public.pem + + +# cors url ์„ค์ • +cors: + url: + - http://localhost:5173 + - http://127.0.0.1:5173 + - http://localhost:5174 + - http://127.0.0.1:5174 + + +# actuator ๋…ธ์ถœ ๋ฒ”์œ„, ์กฐ๊ฑด +management: + endpoints: + web: + exposure: + include: '*' + health: + show-details: always + endpoint: + gateway: + access: unrestricted + +logging: + level: + org.springframework.cloud.gateway: DEBUG + org.springframework.cloud.gateway.discovery: DEBUG + org.springframework.cloud.kubernetes.discovery: DEBUG + reactor.netty.http.client.HttpClient: DEBUG + +springdoc: + swagger-ui: + urls: + - name: User + url: /api/user/v3/api-docs + + - name: Payment + url: /api/payments/v3/api-docs + + - name: Booking + url: /api/booking/v3/api-docs + + - name: Festival + url: /api/festival/v3/api-docs \ No newline at end of file diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml new file mode 100644 index 0000000..38621af --- /dev/null +++ b/src/main/resources/application-docker.yml @@ -0,0 +1,124 @@ +server: + port: 10000 + +spring: + application: + name: api-gateway + cloud: + gateway: + # โ˜… Gateway ๋ฉ”ํŠธ๋ฆญ ํ™œ์„ฑํ™” + metrics: + enabled: true + observation: + enabled: true + server: + webflux: + # โ˜… WebFlux ๋ฉ”ํŠธ๋ฆญ ํ™œ์„ฑํ™” + metrics: + enabled: true + routes: + # ========== User ========== + - id: user + uri: http://user:8080 + predicates: + - Path=/api/user/**,/api/addresses/**,/api/admin/**,/api/users/**,/api/mail/**,/api/auth/**,/api/myPage/** + - id: user-swagger + uri: http://user:8080 + predicates: + - Path=/api/user/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + # ========== Payment ========== + - id: payment + uri: http://payment:8081 + predicates: + - Path=/api/payments/**,/api/tekcitpay/** + - id: payment-swagger + uri: http://payment:8081 + predicates: + - Path=/api/payments/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + # ========== Booking ========== + - id: booking + uri: http://booking:8082 + predicates: + - Path=/api/booking/**,/api/qr/**,/api/host/**,/api/captcha/**,/api/transfer/**,/api/ticket/**,/api/statistics/**,/ws/** + - id: booking_ws_route + uri: ws://booking:8082 + predicates: + - Path=/ws/** + - id: booking-swagger + uri: http://booking:8082 + predicates: + - Path=/api/booking/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + # ========== Festival ========== + - id: festival + uri: http://festival:8083 + predicates: + - Path=/api/festival/**,/uploads/** + - id: festival-swagger + uri: http://festival:8083 + predicates: + - Path=/api/festival/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + +springdoc: + swagger-ui: + urls: + - { name: User, url: /api/user/v3/api-docs } + - { name: Payment, url: /api/payments/v3/api-docs } + - { name: Booking, url: /api/booking/v3/api-docs } + - { name: Festival, url: /api/festival/v3/api-docs } + +cors: + url: + - http://localhost:5173 + - http://127.0.0.1:5173 + - http://localhost:5174 + - http://127.0.0.1:5174 + +jwt: + private-pem-path: ${JWT_PRIVATE_PEM_PATH:file:/etc/keys/private.pem} + public-pem-path: ${JWT_PUBLIC_PEM_PATH:file:/etc/keys/public.pem} + +management: + endpoints: + web: + exposure: + include: + - health + - prometheus + endpoint: + health: + show-details: always + probes: + enabled: true + gateway: + access: unrestricted + metrics: + # โ˜… ๋ฐฑ๋ถ„์œ„ ๊ณ„์‚ฐ์šฉ ํžˆ์Šคํ† ๊ทธ๋žจ + distribution: + percentiles-histogram: + http.server.requests: true + spring.cloud.gateway.requests: true + # (์„ ํƒ) ๋ชจ๋“  ๋ฉ”ํŠธ๋ฆญ์— app ํƒœ๊ทธ ๋ถ€์—ฌ + tags: + application: ${spring.application.name:api-gateway} + +logging: + level: + org.springframework.cloud.gateway: DEBUG + org.springframework.cloud.gateway.discovery: DEBUG + org.springframework.cloud.kubernetes.discovery: DEBUG + reactor.netty.http.client.HttpClient: DEBUG \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 23a31be..f1df18e 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -5,40 +5,123 @@ spring: application: name: api-gateway cloud: - kubernetes: - discovery: - enabled: true - namespaces: - - booking - - festival - - payment - - user gateway: - routes: - - id: local-service - uri: http://localhost:10003 - predicates: - - Path=/api/** + # โ˜… Gateway ๋ฉ”ํŠธ๋ฆญ ํ™œ์„ฑํ™” + metrics: + enabled: true + server: + webflux: + # โ˜… WebFlux ๋ฉ”ํŠธ๋ฆญ ํ™œ์„ฑํ™” + metrics: + enabled: true + # ๊ธฐ์กด ๋ผ์šฐํŒ… ์œ ์ง€(๋กœ์ปฌ ์‹คํ–‰์šฉ localhost ๋Œ€์ƒ) + routes: + - id: user + uri: http://localhost:8080 + predicates: + - Path=/api/user/**,/api/addresses/**,/api/admin/**,/api/users/**,/api/mail/**,/api/auth/**,/api/myPage/** + - id: user-swagger + uri: http://localhost:8080 + predicates: + - Path=/api/user/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + - id: payment + uri: http://localhost:8081 + predicates: + - Path=/api/payments/**,/api/tekcitpay/** + - id: payment-swagger + uri: http://localhost:8081 + predicates: + - Path=/api/payments/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + - id: booking + uri: http://localhost:8082 + predicates: + # โ˜… capcha โ†’ captcha ๋กœ ์˜คํƒˆ์ž ์ˆ˜์ • + - Path=/api/booking/**,/api/qr/**,/api/host/**,/api/captcha/**,/api/transfer/**,/api/ticket/**,/api/statistics/**,/ws/** + - id: booking_ws_route + uri: ws://localhost:8082 + predicates: + - Path=/ws/** + + - id: booking-swagger + uri: http://localhost:8082 + predicates: + - Path=/api/booking/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + - id: festival + uri: http://localhost:8083 + predicates: + - Path=/api/festival/**,/uploads/** + - id: festival-swagger + uri: http://localhost:8083 + predicates: + - Path=/api/festival/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + jwt: private-pem-path: classpath:keys/private.pem public-pem-path: classpath:keys/public.pem - +# cors url ์„ค์ • +cors: + url: + - http://localhost:5173 + - http://127.0.0.1:5173 + - http://localhost:5174 + - http://127.0.0.1:5174 management: endpoints: web: exposure: - include: '*' + include: + - health + - prometheus + endpoint: health: show-details: always - endpoint: + probes: + enabled: true gateway: access: unrestricted + metrics: + # โ˜… ํžˆ์Šคํ† ๊ทธ๋žจ ํ™œ์„ฑํ™”(๋ฐฑ๋ถ„์œ„ ๊ณ„์‚ฐ) + distribution: + percentiles-histogram: + http.server.requests: true + spring.cloud.gateway.requests: true logging: level: org.springframework.cloud.gateway: DEBUG org.springframework.cloud.gateway.discovery: DEBUG org.springframework.cloud.kubernetes.discovery: DEBUG - reactor.netty.http.client.HttpClient: DEBUG \ No newline at end of file + reactor.netty.http.client.HttpClient: DEBUG + +springdoc: + swagger-ui: + urls: + - name: User + url: /api/user/v3/api-docs + + # โ˜… payments ๋กœ ๊ฒฝ๋กœ ์ •ํ•ฉ์„ฑ ์ˆ˜์ • + - name: Payment + url: /api/payments/v3/api-docs + + - name: Booking + url: /api/booking/v3/api-docs + + - name: Festival + url: /api/festival/v3/api-docs \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index cf61e49..195db4a 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,7 +1,11 @@ +server: + port: 8080 + spring: application: name: api-gateway cloud: + # Kubernetes ์ž๋™ Routing ์„ค์ • ๋ฐ ๊ฒ€์ƒ‰ ๋ฒ”์œ„ namespace ์„ค์ • kubernetes: discovery: enabled: true @@ -10,31 +14,89 @@ spring: - festival - payment - user + # ๊ฒ€์ƒ‰ ์กฐ๊ฑด : service ๊ฐ€ ์ด ๋ผ๋ฒจ์„ ํฌํ•จํ•  ๊ฒฝ์šฐ service-labels: expose-via-spring-gateway: "true" gateway: - default-filters: - - StripPrefix=1 - discovery: - locator: - enabled: true - lower-case-service-id: true - predicates: - - name: Path - args: - pattern: "'/' + serviceId + '/**'" - filters: - - name: StripPrefix - args: - parts: 1 + server: + webflux: + routes: + - id: user + uri: http://api-user-service.user.svc.cluster.local:8080 + predicates: + - Path=/api/user/**,/api/addresses/**,/api/admin/**,/api/users/**,/api/mail/**,/api/auth/**,/api/myPage/** + + - id: user-swagger + uri: http://api-user-service.user.svc.cluster.local:8080 + predicates: + - Path=/api/user/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + + - id: payment + uri: http://api-payment-service.payment.svc.cluster.local:8080 + predicates: + - Path=/api/payments/**,/api/tekcitpay/** + + - id: payment-swagger + uri: http://api-payment-service.payment.svc.cluster.local:8080 + predicates: + - Path=/api/payments/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + + - id: booking + uri: http://api-booking-service.booking.svc.cluster.local:8080 + predicates: + - Path=/api/booking/**,/api/qr/**,/api/host/**,/api/captcha/**,/api/transfer/**,/api/ticket/**,/api/statistics/**,/ws/** + - id: booking_ws_route + uri: ws://api-booking-service.booking.svc.cluster.local:8080 + predicates: + - Path=/ws/** + - id: booking-swagger + uri: http://api-booking-service.booking.svc.cluster.local:8080 + predicates: + - Path=/api/booking/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + - id: festival + uri: http://api-festival-service.festival.svc.cluster.local:8080 + predicates: + - Path=/api/festival/**,/uploads/** + - id: festival-swagger + uri: http://api-festival-service.festival.svc.cluster.local:8080 + predicates: + - Path=/api/festival/v3/api-docs + filters: + - StripPrefix=2 + order: -1 + + +# JWT key ์„ธํŒ… jwt: private-pem-path: classpath:keys/private.pem public-pem-path: classpath:keys/public.pem -server: - port: 8080 +# cors url ์„ค์ • +cors: + url: + - http://localhost:5173 + - http://127.0.0.1:5173 + - http://localhost:5174 + - http://127.0.0.1:5174 + - https://www.rookies-tekcit.com + - http://www.rookies-tekcit.com + + +# actuator ๋…ธ์ถœ ๋ฒ”์œ„, ์กฐ๊ฑด management: endpoints: web: @@ -51,4 +113,19 @@ logging: org.springframework.cloud.gateway: DEBUG org.springframework.cloud.gateway.discovery: DEBUG org.springframework.cloud.kubernetes.discovery: DEBUG - reactor.netty.http.client.HttpClient: DEBUG \ No newline at end of file + reactor.netty.http.client.HttpClient: DEBUG + +springdoc: + swagger-ui: + urls: + - name: User + url: /api/user/v3/api-docs + + - name: Payment + url: /api/payments/v3/api-docs + + - name: Booking + url: /api/booking/v3/api-docs + + - name: Festival + url: /api/festival/v3/api-docs \ No newline at end of file