Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
937a57e
build: CI 설정 파일 추가
GoGradually Dec 30, 2025
efe1bea
feat: JWT 키 경로 추가
GoGradually Dec 30, 2025
c339cb8
feat: pinit-gateway 배포 설정 추가
GoGradually Dec 30, 2025
dee419f
feat: CORS 설정 추가 및 JWT 키 경로 수정
GoGradually Dec 30, 2025
abcbaa6
feat: JWT 라이브러리 추가
GoGradually Dec 30, 2025
be9e8aa
feat: JWT 필터 추가 및 CORS 설정 수정
GoGradually Dec 30, 2025
a1091a5
feat: pinit-gateway CD 설정 추가
GoGradually Dec 30, 2025
517ea1f
feat: deployment.yaml에서 환경 변수 설정 제거
GoGradually Dec 30, 2025
e40c034
test: application.yml 및 JWT 공개 키 파일 추가
GoGradually Dec 30, 2025
125663e
feat: CORS 설정을 위한 CorsProperties 클래스 추가
GoGradually Dec 30, 2025
38f68a4
feat: deployment.yaml에서 gRPC 포트 설정 제거
GoGradually Dec 30, 2025
e0ef3d7
fix: pinit-task의 이름을 pinit-gateway로 변경
GoGradually Dec 30, 2025
d7b4104
feat: ingress.yaml에서 서비스 이름을 gateway-service로 변경
GoGradually Dec 30, 2025
3eca4d9
feat: service.yaml 파일 추가 및 gateway-service 설정
GoGradually Dec 30, 2025
64eeb74
feat: JwtAuthenticationToken 클래스 추가
GoGradually Dec 30, 2025
a4e3b48
feat: JwtTokenProvider 클래스 추가 및 JWT 토큰 검증 기능 구현
GoGradually Dec 30, 2025
028e7c8
feat: JwtAuthenticationProvider 클래스 추가 및 JWT 인증 기능 구현
GoGradually Dec 30, 2025
3903516
feat: JwtAuthenticationFilter 클래스 추가 및 JWT 인증 필터링 기능 구현
GoGradually Dec 30, 2025
3aa5464
feat: RsaKeyProvider 클래스 추가 및 RSA 공개 키 로딩 기능 구현
GoGradually Dec 30, 2025
d7bf69d
feat: SecurityConfig 클래스 추가 및 JWT 인증 설정 구현
GoGradually Dec 30, 2025
477dd9d
feat: JwtSubToMemberIdHeaderGatewayFilterFactory 클래스 추가 및 JWT sub을 X-…
GoGradually Dec 30, 2025
1b99af8
docs: 코드 리뷰 코멘트 작성 지침 추가
GoGradually Dec 30, 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
101 changes: 101 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: pinit-gateway CD

on:
push:
branches: [ "master" ]

permissions:
contents: read
packages: write

jobs:
build-test-push-deploy:
runs-on: [ arc-runner-set ]

env:
IMAGE_REPO: ghcr.io/pinit-scheduler/pinit-gateway/app
NAMESPACE: pinit
DEPLOYMENT_NAME: pinit-gateway
CONTAINER_NAME: app
MANIFEST_PATH: k8s/deployment.yaml

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: gradle

- name: Build & Test
run: ./gradlew clean test build

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build & Push Image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ env.IMAGE_REPO }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Install kubectl (if needed)
uses: azure/setup-kubectl@v4
with:
version: v1.33.6

- name: Create kubeconfig from in-cluster ServiceAccount
shell: bash
run: |
TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
CA_PATH="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"

cat > kubeconfig <<EOF
apiVersion: v1
kind: Config
clusters:
- name: in-cluster
cluster:
server: https://kubernetes.default.svc
certificate-authority: ${CA_PATH}
contexts:
- name: in-cluster
context:
cluster: in-cluster
namespace: ${NAMESPACE}
user: sa
current-context: in-cluster
users:
- name: sa
user:
token: ${TOKEN}
EOF

echo "KUBECONFIG=$PWD/kubeconfig" >> $GITHUB_ENV

- name: Install envsubst
run: sudo apt-get update && sudo apt-get install -y gettext-base

- name: Deploy (apply manifest with GITHUB_SHA substitution)
shell: bash
run: |
command -v envsubst >/dev/null 2>&1 || (echo "envsubst not found" && exit 1)

export GITHUB_SHA="${{ github.sha }}"
envsubst < "${MANIFEST_PATH}" | kubectl apply -f -

- name: Rollout status
run: kubectl rollout status deployment/${{ env.DEPLOYMENT_NAME }} -n ${{ env.NAMESPACE }} --timeout=180s
25 changes: 25 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: pinit-gateway CI

on:
pull_request:

permissions:
contents: read

jobs:
build-test:
runs-on: [ arc-runner-set ]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: gradle

- name: Build & Test
run: ./gradlew clean test build
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-security'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
82 changes: 82 additions & 0 deletions k8s/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: pinit-gateway
namespace: pinit
labels:
app: pinit-gateway
spec:
replicas: 2
revisionHistoryLimit: 3
selector:
matchLabels:
app: pinit-gateway

strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0

template:
metadata:
labels:
app: pinit-gateway
spec:
imagePullSecrets:
- name: ghcr-pull-secret
terminationGracePeriodSeconds: 30
volumes:
- name: keys
secret:
secretName: pinit-keys
defaultMode: 0444
containers:
- name: app
image: ghcr.io/pinit-scheduler/pinit-gateway/app:${GITHUB_SHA} # GITHUB_SHA 환경변수는 GitHub Actions에서 설정되며 envsubst로 치환됩니다.
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is in Korean while the code uses English. For consistency, code comments should be in English.

Suggested change
image: ghcr.io/pinit-scheduler/pinit-gateway/app:${GITHUB_SHA} # GITHUB_SHA 환경변수는 GitHub Actions에서 설정되며 envsubst로 치환됩니다.
image: ghcr.io/pinit-scheduler/pinit-gateway/app:${GITHUB_SHA} # The GITHUB_SHA environment variable is set by GitHub Actions and substituted using envsubst.

Copilot uses AI. Check for mistakes.
imagePullPolicy: IfNotPresent

env:
- name: SPRING_PROFILES_ACTIVE
value: prod

volumeMounts:
- mountPath: /etc/keys
name: keys
readOnly: true


ports:
- name: http
containerPort: 8080

readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 6

livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3

resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"

lifecycle:
preStop:
exec:
command: [ "sh", "-c", "sleep 5" ]
6 changes: 3 additions & 3 deletions k8s/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ spec:
pathType: Prefix
backend:
service:
name: auth-service
name: gateway-service
port:
number: 80
- host: api.pinit.go-gradually.me
Expand All @@ -32,7 +32,7 @@ spec:
pathType: Prefix
backend:
service:
name: task-service
name: gateway-service
port:
number: 80
- host: notification.pinit.go-gradually.me
Expand All @@ -42,6 +42,6 @@ spec:
pathType: Prefix
backend:
service:
name: notification-service
name: gateway-service
port:
number: 80
14 changes: 14 additions & 0 deletions k8s/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: gateway-service
namespace: pinit
spec:
selector:
app: pinit-gateway
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP

Comment on lines +13 to +14
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace detected at the end of the file. This should be removed for cleaner code.

Suggested change
type: ClusterIP
type: ClusterIP

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package me.pinitgateway.filter;

import me.pinitgateway.jwt.JwtAuthenticationToken;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

@Component
public class JwtSubToMemberIdHeaderGatewayFilterFactory
extends AbstractGatewayFilterFactory<JwtSubToMemberIdHeaderGatewayFilterFactory.Config> {

public JwtSubToMemberIdHeaderGatewayFilterFactory() {
super(Config.class);
}

public static class Config {
}

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) ->
exchange.getPrincipal()
.filter(Authentication.class::isInstance)
.cast(Authentication.class)
.flatMap(auth -> {
String sub = extractSub(auth);

// JWT 검증은 SecurityWebFilterChain에서 이미 처리되었으므로, sub이 없으면 그대로 진행
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is in Korean while code uses English. For consistency, code comments should be in English.

Copilot uses AI. Check for mistakes.
if (sub == null || sub.isBlank()) {
return chain.filter(exchange);
}

ServerWebExchange mutated = mutateHeader(exchange, sub);
return chain.filter(mutated);
})
.switchIfEmpty(chain.filter(exchange));
}

private String extractSub(Authentication auth) {
if (auth instanceof JwtAuthenticationToken jwtAuth) {
return String.valueOf(jwtAuth.getPrincipal()); // = sub
}
return null;
}

private ServerWebExchange mutateHeader(ServerWebExchange exchange, String sub) {
var request = exchange.getRequest().mutate()
// 외부에서 X-Member-Id를 임의로 넣어오는 spoofing 방지
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is in Korean while code and other comments use English. For consistency, code comments should be in English.

Copilot uses AI. Check for mistakes.
.headers(headers -> headers.remove("X-Member-Id"))
.header("X-Member-Id", sub)
.build();

return exchange.mutate().request(request).build();
}
}
48 changes: 48 additions & 0 deletions src/main/java/me/pinitgateway/jwt/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package me.pinitgateway.jwt;

import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

public class JwtAuthenticationFilter implements WebFilter {

private final ReactiveAuthenticationManager authenticationManager;

public JwtAuthenticationFilter(ReactiveAuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String token = resolveToken(exchange);

if (!StringUtils.hasText(token)) {
return chain.filter(exchange);
}

return authenticationManager.authenticate(new JwtAuthenticationToken(token))
.flatMap(authentication ->
chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)))
.switchIfEmpty(chain.filter(exchange))
.onErrorResume(ex -> handleAuthenticationError(exchange));
}

private Mono<Void> handleAuthenticationError(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
Comment on lines +36 to +38
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authentication error handler returns a generic 401 Unauthorized status without a response body. This provides no information about why authentication failed, making it difficult for API consumers to debug issues. Consider adding an error message body with details about the authentication failure.

Copilot uses AI. Check for mistakes.
}

private String resolveToken(ServerWebExchange exchange) {
String bearer = exchange.getRequest().getHeaders().getFirst("Authorization");
if(StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
Loading