|
1 | | -# API Gateway Service |
2 | | - |
3 | | -이 프로젝트는 레슨 매칭 플랫폼(Lesson Matching Platform)의 진입점 역할을 하는 **Spring Cloud Gateway** 서비스입니다. 모든 클라이언트 요청은 이 게이트웨이를 거쳐 내부 마이크로서비스로 라우팅되며, 공통적인 인증 및 인가 처리를 담당합니다. |
4 | | - |
5 | | -## 🛠️ 기술 스택 (Tech Stack) |
6 | | -- **Java**: 21 |
7 | | -- **Framework**: Spring Boot 4.1.0, Spring Cloud 2025.1.2 |
8 | | -- **Gateway**: Spring Cloud Gateway (WebFlux 기반) |
9 | | -- **Security**: Spring Security WebFlux, JWT (JSON Web Token) |
10 | | -- **Build Tool**: Gradle |
11 | | - |
12 | | -## ✨ 주요 기능 (Features) |
13 | | - |
14 | | -### 1. API 라우팅 (Routing) |
15 | | -`application.yml`에 정의된 라우팅 규칙에 따라 클라이언트의 요청을 적절한 백엔드 마이크로서비스로 포워딩합니다. |
16 | | -- `/auth/**` : 인증 서비스(`AUTH_SERVICE_HOST`)로 라우팅 |
17 | | -- `/get/**` : 목업 서버(`MOCK_SERER`)로 라우팅 |
18 | | -- 요청을 전달할 때 게이트웨이를 거쳤음을 알리기 위해 `X-Request-Source: Gateway` 헤더를 추가합니다. |
19 | | - |
20 | | -### 2. 보안 및 인증 (Security & Authentication) |
21 | | -- **JWT 기반 인증**: 클라이언트로부터 전달받은 JWT 토큰의 유효성을 검사하여 인증을 수행합니다 (`JwtAuthenticationManager`, `JwtAuthenticationConverter`). |
22 | | -- **권한 설정**: |
23 | | - - `/auth/**`, `/public/**` 엔드포인트는 인증 없이 접근할 수 있습니다. (Permit All) |
24 | | - - 그 외의 모든 요청은 인증을 거쳐야 합니다. |
25 | | -- WebFlux 전용 Security 설정(`@EnableWebFluxSecurity`)을 사용하며, 무상태(Stateless) API 통신을 위해 CSRF, Form Login, HTTP Basic 기능은 비활성화되어 있습니다. |
26 | | - |
27 | | -### 3. 사용자 정보 전달 필터 (UserInfoFilter) |
28 | | -- 인가된 사용자의 요청이 게이트웨이를 통과할 때 동작하는 전역 필터(Global Filter)입니다. |
29 | | -- SecurityContext에서 인증 객체(JWT 파싱 결과)를 가져와 `userId`와 `role` 정보를 추출합니다. |
30 | | -- 하위 마이크로서비스로 요청을 전달할 때, **`X-User-Id`** 및 **`X-User-Role`** HTTP 헤더에 해당 정보를 담아 전달합니다. |
31 | | -- 이를 통해 개별 마이크로서비스는 별도의 JWT 검증 과정 없이 헤더 정보만으로 요청한 사용자를 식별할 수 있습니다. |
32 | | - |
33 | | -## 📂 프로젝트 구조 (Project Structure) |
| 1 | +# Lesson Matching Platform API Gateway |
| 2 | + |
| 3 | +Lesson Matching Platform의 외부 진입점입니다. Spring Cloud Gateway(WebFlux)를 기반으로 클라이언트 요청을 내부 서비스로 전달하고, JWT 인증·CORS·요청 추적·사용자 정보 전달을 공통 처리합니다. |
| 4 | + |
| 5 | +## Role in the Platform |
| 6 | + |
| 7 | +```mermaid |
| 8 | +flowchart LR |
| 9 | + Client["Client"] --> Gateway["API Gateway\nSpring Cloud Gateway"] |
| 10 | + Gateway --> Backend["Core Backend"] |
| 11 | + Gateway --> Ledger["Ledger"] |
| 12 | + Gateway --> Notice["Notice Server"] |
| 13 | +``` |
| 14 | + |
| 15 | +게이트웨이는 비즈니스 로직을 직접 처리하기보다 모든 서비스 요청에 공통으로 필요한 경계 책임을 담당합니다. |
| 16 | + |
| 17 | +## Responsibilities |
| 18 | + |
| 19 | +### 1. Service Routing |
| 20 | + |
| 21 | +라우팅 대상 주소는 환경 변수로 주입합니다. 로컬에서는 기본값으로 `localhost`를 사용하고, GCP에서는 Terraform이 내부 DNS 호스트명을 전달합니다. |
| 22 | + |
| 23 | +| Incoming path | Destination | Gateway behavior | |
| 24 | +| --- | --- | --- | |
| 25 | +| `/api/**` | Core Backend | `X-Request-Source: Gateway` 추가 | |
| 26 | +| `/resources/files/**` | Core Backend | 파일 리소스 전달, 인증 예외 | |
| 27 | +| `/api/v1/ledger-entry/**` | Ledger | 앞의 두 경로 세그먼트 제거 후 전달 | |
| 28 | +| `/api/v1/notification/**` | Notice Server | `X-Request-Source: Gateway` 추가 | |
| 29 | +| `/test` | Mock Server | 테스트용 경로 재작성 | |
| 30 | + |
| 31 | +주요 환경 변수: |
| 32 | + |
| 33 | +```text |
| 34 | +BACKEND_HOST # Core Backend host |
| 35 | +LEDGER_HOST # Ledger host |
| 36 | +NOTIFICATION_HOST # Notice Server host |
| 37 | +``` |
| 38 | + |
| 39 | +### 2. JWT Authentication |
| 40 | + |
| 41 | +- `Authorization: Bearer <token>` 형식의 JWT를 추출합니다. |
| 42 | +- `JwtProvider`가 서명과 만료 여부를 검증합니다. |
| 43 | +- JWT claims에서 `userId`, `role`을 읽어 Reactive Security Context에 저장합니다. |
| 44 | +- 인증이 필요한 요청은 검증에 실패하면 내부 서비스로 전달하지 않습니다. |
| 45 | +- CSRF, Form Login, HTTP Basic은 stateless API 특성에 맞게 비활성화했습니다. |
| 46 | + |
| 47 | +인증 없이 접근할 수 있는 주요 경로는 다음과 같습니다. |
| 48 | + |
| 49 | +- `OPTIONS /**` |
| 50 | +- `GET /api/v1/categories` |
| 51 | +- `GET /api/v1/lessons/search` |
| 52 | +- `GET /api/v1/lessons/{lessonId}` |
| 53 | +- `GET /api/v1/lessons/{lessonId}/reviews` |
| 54 | +- `POST /api/v1/auth/login` |
| 55 | +- `POST /api/v1/members/signup` |
| 56 | +- `/auth/**`, `/public/**`, `/actuator/**` |
| 57 | +- `/resources/files/**`, `/test` |
| 58 | + |
| 59 | +그 외 요청은 인증된 JWT가 필요합니다. |
| 60 | + |
| 61 | +### 3. User Context Propagation |
| 62 | + |
| 63 | +인증된 요청은 `UserInfoFilter`를 통해 아래 헤더를 내부 서비스로 전달합니다. |
| 64 | + |
| 65 | +```text |
| 66 | +X-User-Id # JWT의 userId claim |
| 67 | +X-User-Role # JWT의 role claim |
| 68 | +``` |
| 69 | + |
| 70 | +내부 서비스가 요청마다 JWT를 다시 파싱하지 않고도 인증된 사용자의 식별자와 역할을 사용할 수 있도록 공통 경계를 제공합니다. |
| 71 | + |
| 72 | +### 4. Request ID Propagation |
| 73 | + |
| 74 | +`RequestIdFilter`는 모든 요청에 `X-Request-Id`를 부여합니다. |
| 75 | + |
| 76 | +- 클라이언트가 유효한 `X-Request-Id`를 보내면 기존 값을 유지합니다. |
| 77 | +- 값이 없으면 UUID를 생성합니다. |
| 78 | +- 요청 헤더와 응답 헤더에 동일한 ID를 설정합니다. |
| 79 | +- MDC에 ID를 넣어 로그에서 요청 단위 추적이 가능하도록 합니다. |
| 80 | + |
| 81 | +### 5. CORS and Observability |
| 82 | + |
| 83 | +- 로컬 프론트엔드(`localhost:3000`, `127.0.0.1:3000`)를 CORS 허용 origin으로 설정합니다. |
| 84 | +- `Authorization`, `Content-Type`, `Accept` 등의 요청 헤더를 허용합니다. |
| 85 | +- `/actuator/health`, `/actuator/info`, `/actuator/prometheus`를 노출합니다. |
| 86 | +- Prometheus 메트릭에 애플리케이션 이름을 태그로 추가합니다. |
| 87 | + |
| 88 | +## Authentication Flow |
| 89 | + |
| 90 | +```mermaid |
| 91 | +sequenceDiagram |
| 92 | + participant Client |
| 93 | + participant Gateway |
| 94 | + participant Service as Internal Service |
| 95 | + Client->>Gateway: Request + Bearer JWT |
| 96 | + Gateway->>Gateway: Validate signature and claims |
| 97 | + Gateway->>Gateway: Create or preserve X-Request-Id |
| 98 | + Gateway->>Service: X-User-Id / X-User-Role / X-Request-Id |
| 99 | + Service-->>Gateway: Response |
| 100 | + Gateway-->>Client: Response + X-Request-Id |
| 101 | +``` |
| 102 | + |
| 103 | +## Project Structure |
| 104 | + |
34 | 105 | ```text |
35 | 106 | src/main/java/com/hwan/gateway/ |
36 | | - ├── config/ |
37 | | - │ └── SecurityConfig.java # Spring Security WebFlux 설정 (경로 권한 및 필터 등록) |
38 | | - ├── controller/ |
39 | | - │ └── AuthController.java # 게이트웨이 레벨의 인증 관련 컨트롤러 |
40 | | - ├── filter/ |
41 | | - │ └── UserInfoFilter.java # 인증된 사용자 정보를 헤더에 주입하는 Global Filter |
42 | | - ├── jwt/ |
43 | | - │ ├── JwtAuthenticationConverter.java # HTTP 요청에서 JWT 토큰을 추출/변환 |
44 | | - │ ├── JwtAuthenticationManager.java # JWT 토큰 검증 및 Authentication 객체 생성 |
45 | | - │ └── JwtProvider.java # JWT 토큰 생성 및 파싱 유틸리티 |
46 | | - └── GatewayApplication.java # 메인 애플리케이션 클래스 |
47 | | -``` |
48 | | - |
49 | | -## ⚙️ 주요 설정 (application.yml) |
50 | | -- 포트: `8080` |
51 | | -- 환경 변수 및 Secret Manager 연동 (`import: sm://`) |
52 | | -- 라우팅 및 Predicate, Filter 설정 |
53 | | -- JWT 시크릿 키 설정 (`jwt.secret`) |
| 107 | +├── config/ |
| 108 | +│ └── SecurityConfig.java # WebFlux Security, CORS, public paths |
| 109 | +├── controller/ |
| 110 | +│ └── AuthController.java # Token endpoint for local/dev use |
| 111 | +├── filter/ |
| 112 | +│ ├── RequestIdFilter.java # Request ID 생성·전파·MDC 기록 |
| 113 | +│ └── UserInfoFilter.java # 사용자 정보 헤더 전달 |
| 114 | +├── jwt/ |
| 115 | +│ ├── JwtAuthenticationConverter.java # Bearer token 추출 |
| 116 | +│ ├── JwtAuthenticationManager.java # JWT 검증과 Authentication 생성 |
| 117 | +│ └── JwtProvider.java # JWT 생성·파싱·검증 |
| 118 | +└── GatewayApplication.java |
| 119 | +``` |
| 120 | + |
| 121 | +## Tech Stack |
| 122 | + |
| 123 | +- Java 21 |
| 124 | +- Spring Boot 4.1.0 |
| 125 | +- Spring Cloud Gateway 2025.1.2 |
| 126 | +- Spring WebFlux |
| 127 | +- Spring Security WebFlux |
| 128 | +- JJWT 0.12.7 |
| 129 | +- Spring Cloud GCP Secret Manager |
| 130 | +- Spring Boot Actuator + Micrometer Prometheus Registry |
| 131 | +- Gradle |
| 132 | + |
| 133 | +## Configuration |
| 134 | + |
| 135 | +JWT secret은 코드나 설정 파일에 직접 저장하지 않고 Spring Cloud GCP Secret Manager import를 통해 주입합니다. |
| 136 | + |
| 137 | +```yaml |
| 138 | +spring: |
| 139 | + config: |
| 140 | + import: sm:// |
| 141 | + |
| 142 | +jwt: |
| 143 | + secret: ${sm://JWT_SECRET_KEY} |
| 144 | +``` |
| 145 | +
|
| 146 | +로컬에서 Secret Manager를 사용하지 않는 경우에는 실행 환경에서 `JWT_SECRET_KEY`를 안전하게 주입하는 별도 설정이 필요합니다. 실제 운영 Secret과 테스트용 Secret을 분리해서 사용해야 합니다. |
| 147 | + |
| 148 | +## Local Development |
| 149 | + |
| 150 | +```bash |
| 151 | +./gradlew bootRun |
| 152 | +``` |
| 153 | + |
| 154 | +기본 포트는 `8080`입니다. 내부 서비스 주소를 로컬에서 지정하려면 다음과 같이 실행할 수 있습니다. |
| 155 | + |
| 156 | +```bash |
| 157 | +BACKEND_HOST=localhost \ |
| 158 | +LEDGER_HOST=localhost \ |
| 159 | +NOTIFICATION_HOST=localhost \ |
| 160 | +./gradlew bootRun |
| 161 | +``` |
| 162 | + |
| 163 | +`/auth/token` 엔드포인트는 `userId`와 `role`을 받아 JWT를 생성합니다. 이 기능은 로컬·개발 테스트 편의를 위한 용도로만 사용하고, 외부에 공개되는 운영 환경에서는 접근을 제한해야 합니다. |
| 164 | + |
| 165 | +예시: |
| 166 | + |
| 167 | +```bash |
| 168 | +curl "http://localhost:8080/auth/token?userId=user-001&role=USER" |
| 169 | +``` |
| 170 | + |
| 171 | +## Build and Test |
| 172 | + |
| 173 | +```bash |
| 174 | +./gradlew test |
| 175 | +./gradlew build |
| 176 | +``` |
| 177 | + |
| 178 | +현재 테스트는 JWT 생성·파싱·검증과 Spring Boot 컨텍스트 구성을 중심으로 구성되어 있습니다. |
| 179 | + |
| 180 | +## CI/CD |
| 181 | + |
| 182 | +`.github/workflows/gradle.yml`은 `main` 브랜치 push를 기준으로 다음 작업을 수행합니다. |
| 183 | + |
| 184 | +1. Java 21 환경 구성 |
| 185 | +2. Gradle build |
| 186 | +3. GitHub Actions Workload Identity Federation으로 GCP 인증 |
| 187 | +4. Docker 이미지 빌드 |
| 188 | +5. Artifact Registry에 이미지 push |
| 189 | +6. `api-gateway` Compute Engine VM 재시작 |
| 190 | + |
| 191 | +GCP 인증 정보는 GitHub Actions Secret과 Variable로 관리하며 저장소에 직접 기록하지 않습니다. |
| 192 | + |
| 193 | +## Security Notes |
| 194 | + |
| 195 | +- JWT secret은 Secret Manager에서 관리합니다. |
| 196 | +- 내부 서비스는 외부에 직접 노출하지 않고 Gateway를 통해 접근하도록 구성하는 것을 전제로 합니다. |
| 197 | +- `X-User-Id`, `X-User-Role`은 내부 네트워크에서만 신뢰해야 하며, 내부 서비스 포트가 외부에 공개되지 않도록 방화벽을 제한해야 합니다. |
| 198 | +- `/auth/token`은 테스트용 토큰 발급 기능이므로 운영 환경에서 비활성화하거나 접근을 제한해야 합니다. |
| 199 | +- CORS 허용 origin은 실제 배포 도메인에 맞게 최소 범위로 설정해야 합니다. |
0 commit comments