diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..823f254 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +- 코드 리뷰 코멘트는 항상 한국어로 작성한다. +- 변경사항별로: (1) 문제점 (2) 영향 (3) 수정 제안 순서로 작성한다. +- 보안/성능/동시성 이슈를 우선 점검한다. \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..2ff8ed3 --- /dev/null +++ b/.github/workflows/cd.yml @@ -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 <> $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 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..ac5d3ca --- /dev/null +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/build.gradle b/build.gradle index ab15e27..f972125 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..aa5eb61 --- /dev/null +++ b/k8s/deployment.yaml @@ -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로 치환됩니다. + 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" ] diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml index 5c596fc..5f2fbd7 100644 --- a/k8s/ingress.yaml +++ b/k8s/ingress.yaml @@ -22,7 +22,7 @@ spec: pathType: Prefix backend: service: - name: auth-service + name: gateway-service port: number: 80 - host: api.pinit.go-gradually.me @@ -32,7 +32,7 @@ spec: pathType: Prefix backend: service: - name: task-service + name: gateway-service port: number: 80 - host: notification.pinit.go-gradually.me @@ -42,6 +42,6 @@ spec: pathType: Prefix backend: service: - name: notification-service + name: gateway-service port: number: 80 diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..427a960 --- /dev/null +++ b/k8s/service.yaml @@ -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 + \ No newline at end of file diff --git a/src/main/java/me/pinitgateway/filter/JwtSubToMemberIdHeaderGatewayFilterFactory.java b/src/main/java/me/pinitgateway/filter/JwtSubToMemberIdHeaderGatewayFilterFactory.java new file mode 100644 index 0000000..2be8102 --- /dev/null +++ b/src/main/java/me/pinitgateway/filter/JwtSubToMemberIdHeaderGatewayFilterFactory.java @@ -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 { + + 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이 없으면 그대로 진행 + 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 방지 + .headers(headers -> headers.remove("X-Member-Id")) + .header("X-Member-Id", sub) + .build(); + + return exchange.mutate().request(request).build(); + } +} diff --git a/src/main/java/me/pinitgateway/jwt/JwtAuthenticationFilter.java b/src/main/java/me/pinitgateway/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..c863682 --- /dev/null +++ b/src/main/java/me/pinitgateway/jwt/JwtAuthenticationFilter.java @@ -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 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 handleAuthenticationError(ServerWebExchange exchange) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return exchange.getResponse().setComplete(); + } + + 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; + } +} diff --git a/src/main/java/me/pinitgateway/jwt/JwtAuthenticationToken.java b/src/main/java/me/pinitgateway/jwt/JwtAuthenticationToken.java new file mode 100644 index 0000000..f812af1 --- /dev/null +++ b/src/main/java/me/pinitgateway/jwt/JwtAuthenticationToken.java @@ -0,0 +1,45 @@ +package me.pinitgateway.jwt; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; +import java.util.Collections; + +public class JwtAuthenticationToken extends AbstractAuthenticationToken { + private final Long memberId; + private final String token; + + public JwtAuthenticationToken(String token) { + super(Collections.emptyList()); + this.memberId = null; + this.token = token; + super.setAuthenticated(false); + } + + public JwtAuthenticationToken(Long principal, String token, Collection authorities) { + super(authorities); + this.memberId = principal; + this.token = token; + super.setAuthenticated(true); + } + + @Override + public String getCredentials() { + return token; + } + + @Override + public Long getPrincipal() { + return memberId; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) { + if (isAuthenticated) { + throw new IllegalArgumentException("인증 상태 설정은 생성자에서만 할 수 있습니다."); + } + super.setAuthenticated(false); + } + +} diff --git a/src/main/java/me/pinitgateway/jwt/JwtTokenProvider.java b/src/main/java/me/pinitgateway/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..e6faff7 --- /dev/null +++ b/src/main/java/me/pinitgateway/jwt/JwtTokenProvider.java @@ -0,0 +1,60 @@ +package me.pinitgateway.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.stream.Collectors; + +public class JwtTokenProvider { + private final PublicKey publicKey; + + public JwtTokenProvider(PublicKey publicKey) { + this.publicKey = publicKey; + } + + public boolean validateToken(String token) { + try { + Claims claims = parse(token); + return !claims.getExpiration().before(new Date()); + } catch (Exception e) { + return false; + } + } + + public Long getMemberId(String token) { + return Long.parseLong(parse(token).getSubject()); + } + + public Claims parse(String token){ + return buildParser() + .parseSignedClaims(token) + .getPayload(); + } + + public Collection getAuthorities(String token) { + Claims claims = parse(token); + String roles = claims.get("roles", String.class); + if (roles == null || roles.isEmpty()) { + return Collections.emptyList(); + } + return Arrays.stream(roles.split(",")) + .map(String::trim) + .filter(r -> !r.isEmpty()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + private JwtParser buildParser() { + return Jwts.parser() + .verifyWith(publicKey) + .build(); + } +} diff --git a/src/main/java/me/pinitgateway/jwt/RsaKeyProvider.java b/src/main/java/me/pinitgateway/jwt/RsaKeyProvider.java new file mode 100644 index 0000000..ab89fbd --- /dev/null +++ b/src/main/java/me/pinitgateway/jwt/RsaKeyProvider.java @@ -0,0 +1,31 @@ +package me.pinitgateway.jwt; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +public class RsaKeyProvider { + public static PublicKey loadPublicKey(String pemPath) { + try { + String key = new String(Files.readAllBytes(Paths.get(pemPath))); + key = key + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s", ""); + + byte[] decoded = Base64.getDecoder().decode(key); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded); + + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(keySpec); + } catch (IOException | GeneralSecurityException e) { + throw new IllegalStateException("Failed to load RSA public key", e); + } + } +} + diff --git a/src/main/java/me/pinitgateway/security/CorsProperties.java b/src/main/java/me/pinitgateway/security/CorsProperties.java new file mode 100644 index 0000000..f47f72a --- /dev/null +++ b/src/main/java/me/pinitgateway/security/CorsProperties.java @@ -0,0 +1,57 @@ +package me.pinitgateway.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "cors") +public class CorsProperties { + + private List allowedOrigins; + private List allowedMethods; + private List allowedHeaders; + private Boolean allowCredentials; + private Long maxAge; + + public List getAllowedOrigins() { + return allowedOrigins; + } + + public void setAllowedOrigins(List allowedOrigins) { + this.allowedOrigins = allowedOrigins; + } + + public List getAllowedMethods() { + return allowedMethods; + } + + public void setAllowedMethods(List allowedMethods) { + this.allowedMethods = allowedMethods; + } + + public List getAllowedHeaders() { + return allowedHeaders; + } + + public void setAllowedHeaders(List allowedHeaders) { + this.allowedHeaders = allowedHeaders; + } + + public Boolean getAllowCredentials() { + return allowCredentials; + } + + public void setAllowCredentials(Boolean allowCredentials) { + this.allowCredentials = allowCredentials; + } + + public Long getMaxAge() { + return maxAge; + } + + public void setMaxAge(Long maxAge) { + this.maxAge = maxAge; + } +} diff --git a/src/main/java/me/pinitgateway/security/JwtAuthenticationProvider.java b/src/main/java/me/pinitgateway/security/JwtAuthenticationProvider.java new file mode 100644 index 0000000..640e5fe --- /dev/null +++ b/src/main/java/me/pinitgateway/security/JwtAuthenticationProvider.java @@ -0,0 +1,41 @@ +package me.pinitgateway.security; + +import me.pinitgateway.jwt.JwtAuthenticationToken; +import me.pinitgateway.jwt.JwtTokenProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import reactor.core.publisher.Mono; + +import java.util.Collection; + +public class JwtAuthenticationProvider implements ReactiveAuthenticationManager { + private final JwtTokenProvider jwtTokenProvider; + + public JwtAuthenticationProvider(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public Mono authenticate(Authentication authentication) throws AuthenticationException { + if(!(authentication instanceof JwtAuthenticationToken)){ + return Mono.empty(); + } + + String token = (String) authentication.getCredentials(); + + return Mono.fromCallable(() -> { + if(!jwtTokenProvider.validateToken(token)) { + throw new BadCredentialsException("Invalid token"); + } + + Long memberId = jwtTokenProvider.getMemberId(token); + Collection authorities = jwtTokenProvider.getAuthorities(token); + + return new JwtAuthenticationToken(memberId, token, authorities); + }); + } +} + diff --git a/src/main/java/me/pinitgateway/security/SecurityConfig.java b/src/main/java/me/pinitgateway/security/SecurityConfig.java new file mode 100644 index 0000000..b1f3afb --- /dev/null +++ b/src/main/java/me/pinitgateway/security/SecurityConfig.java @@ -0,0 +1,140 @@ +package me.pinitgateway.security; + +import me.pinitgateway.jwt.JwtAuthenticationFilter; +import me.pinitgateway.jwt.JwtTokenProvider; +import me.pinitgateway.jwt.RsaKeyProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsConfigurationSource; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; + +import java.net.InetSocketAddress; +import java.security.PublicKey; + +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + + @Value("${path.key.jwt.public}") + private String publicKeyPath; + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityWebFilterChain authPublicChain(ServerHttpSecurity http) { + return applyCommon(http) + .securityMatcher(ServerWebExchangeMatchers.matchers(authHostMatcher(), authPathMatcher())) + .authorizeExchange(auth -> auth.anyExchange().permitAll()) + .build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityWebFilterChain mainSecurityFilterChain(ServerHttpSecurity http, + JwtAuthenticationFilter jwtAuthenticationFilter) { + return applyCommon(http) + .authorizeExchange(auth -> auth + .pathMatchers("/actuator/health/liveness", "/actuator/health/readiness", "/v3/**", "/swagger-ui/**", "/async-api/**").permitAll() + .anyExchange().authenticated() + ) + .addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION) + .build(); + } + + @Bean + public ReactiveAuthenticationManager authenticationManager(JwtAuthenticationProvider jwtAuthenticationProvider) { + return jwtAuthenticationProvider; + } + + @Bean + public JwtAuthenticationFilter jwtAuthenticationFilter(ReactiveAuthenticationManager authenticationManager) { + return new JwtAuthenticationFilter(authenticationManager); + } + + @Bean + public JwtAuthenticationProvider jwtAuthenticationProvider(JwtTokenProvider jwtTokenProvider) { + return new JwtAuthenticationProvider(jwtTokenProvider); + } + + @Bean + public JwtTokenProvider jwtTokenProvider() { + PublicKey publicKey = RsaKeyProvider.loadPublicKey(publicKeyPath); + return new JwtTokenProvider(publicKey); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource(CorsProperties corsProperties) { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(corsProperties.getAllowedOrigins()); + config.setAllowedMethods(corsProperties.getAllowedMethods()); + config.setAllowedHeaders(corsProperties.getAllowedHeaders()); + config.setAllowCredentials(corsProperties.getAllowCredentials()); + config.setMaxAge(corsProperties.getMaxAge()); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + + private ServerHttpSecurity applyCommon(ServerHttpSecurity http) { + return http + .cors(Customizer.withDefaults()) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) + .formLogin(ServerHttpSecurity.FormLoginSpec::disable) + .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()); + } + + private ServerWebExchangeMatcher authHostMatcher() { + return exchange -> { + InetSocketAddress host = exchange.getRequest().getHeaders().getHost(); + if (host == null) { + return ServerWebExchangeMatcher.MatchResult.notMatch(); + } + + String hostString = host.getHostString(); + int port = host.getPort(); + boolean isAuthHost = "auth.pinit.go-gradually.me".equals(hostString) + || ("localhost".equals(hostString) && (port == 8081 || port == -1)); + + return isAuthHost + ? ServerWebExchangeMatcher.MatchResult.match() + : ServerWebExchangeMatcher.MatchResult.notMatch(); + }; + } + + private ServerWebExchangeMatcher authPathMatcher() { + return ServerWebExchangeMatchers.pathMatchers( + "/login", + "/signup", + "/refresh", + "/login/**" + ); + } +} + +/** + * 현재 필터 규칙 + * - /actuator/health/liveness : 인증 안함 + * - /actuator/health/readiness : 인증 안함 + * - /v3/** : 인증 안함 + * - /swagger-ui/** : 인증 안함 + * - /async-api/** : 인증 안함 + * - auth host + /login : 인증 안함 + * - auth host + /signup : 인증 안함 + * - auth host + /refresh : 인증 안함 + * - auth host + /login/** : 인증 안함 + * - 그 외 : 인증 필요 + */ diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index c885594..46cdbdf 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -7,15 +7,26 @@ spring: - id: notification-service uri: http://localhost:8082 predicates: - - Host=http://localhost:8082 + - Host=localhost:8082 + filters: + - JwtSubToMemberIdHeader - id: auth-service uri: http://localhost:8081 predicates: - Host=localhost:8081 + filters: + - JwtSubToMemberIdHeader - id: task-service uri: http://localhost:8080 predicates: - Host=localhost:8080 + filters: + - JwtSubToMemberIdHeader server: - port: 8060 \ No newline at end of file + port: 8060 + +path: + key: + jwt: + public: ${HOME}/pinit/keys/jwt-public-key.pem \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index ae70bd0..18b9468 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -8,11 +8,38 @@ spring: uri: http://notification-service predicates: - Host=notification.pinit.go-gradually.me + filters: + - JwtSubToMemberIdHeader - id: auth-service uri: http://auth-service predicates: - Host=auth.pinit.go-gradually.me + filters: + - JwtSubToMemberIdHeader - id: task-service uri: http://task-service predicates: - - Host=api.pinit.go-gradually.me \ No newline at end of file + - Host=api.pinit.go-gradually.me + filters: + - JwtSubToMemberIdHeader + +path: + key: + jwt: + public: /etc/keys/jwt-public-key.pem + + +cors: + allowed-origins: + - "https://pinit.go-gradually.me" + allowed-methods: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + allowed-headers: + - "*" + allow-credentials: true + max-age: 3600 diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..ad93eb7 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,19 @@ +path: + key: + jwt: + public: src/test/resources/jwt-public-key.pem + +cors: + allowed-origins: + - "*" + allowed-methods: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + allowed-headers: + - "*" + allow-credentials: true + max-age: 3600 diff --git a/src/test/resources/jwt-public-key.pem b/src/test/resources/jwt-public-key.pem new file mode 100644 index 0000000..de1df5e --- /dev/null +++ b/src/test/resources/jwt-public-key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArxev+AlfZ9E4+HGc4vlQ +6GiKvHSjC4qPNSBfRHjt/bn8zB5Kc44aWxcTxMetQ9NTvq+E0/HmNGVOMlIkm+m2 +fxDiZj0XCL9e5g/zFVnzVqMJpjLxzz7sUhEqyCIf/gJMmzW4hCOaT+6HeAxE5hYk +gHDJJEf7+paDC6hNzWLvapiHRn1BOUxFBY90tiZsC3AIa0TgVNOjZzam5++KHhwX +L0oAydGOk/K0ve0ISOv3DP5U6rmKNnbhamXekdGpvsjwhfzfx+jhFPl0bcPcEE9Z +kP11QtjPC4wu3kwdBvZPbIgLFF4JTH19gs+v4bGQGR/BQK+81qWHWL1dtHryAbSj +/QIDAQAB +-----END PUBLIC KEY-----