From 1d88daa29333f4ddbf1cbb354e7d60f6ced5ab4a Mon Sep 17 00:00:00 2001 From: Sehi55 Date: Wed, 29 Apr 2026 22:38:27 +0900 Subject: [PATCH 1/7] feat: add /auth route --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e966ddc..8eb5278 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,7 +11,7 @@ spring: - id: user-service uri: lb://USER-SERVICE predicates: - - Path=/api/v1/users/**,/api/v1/admin/users/** + - Path=/api/v1/users/**,/api/v1/admin/users/**,/api/v1/auth/**,/api/v1/admin/auth/** - id: timeslot-service uri: lb://TIMESLOT-SERVICE From 63aab992ee78c9892349a0a88872ad823e1a70f0 Mon Sep 17 00:00:00 2001 From: Sehi55 Date: Thu, 30 Apr 2026 05:05:30 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20gradle=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20applcation.yml=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++- build.gradle | 9 +++++++++ src/main/resources/application-docker.yml | 2 +- src/main/resources/application-local.yml | 6 +++++- src/main/resources/application-test.yml | 9 ++++++++- src/main/resources/application.yml | 6 ++++++ 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3ad6ce9..fd05c33 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ \ No newline at end of file +.vscode/ + +.env \ No newline at end of file diff --git a/build.gradle b/build.gradle index c2da34b..096a4ae 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,17 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.cloud:spring-cloud-starter-gateway-server-webflux' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml index 73ed6dc..fb8ab06 100644 --- a/src/main/resources/application-docker.yml +++ b/src/main/resources/application-docker.yml @@ -1,4 +1,4 @@ eureka: client: service-url: - defaultZone: http://eureka-server:8761/eureka/ \ No newline at end of file + defaultZone: http://${EUREKA_CONTAINER_NAME}:${EUREKA_PORT}/eureka/ \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index aa7c90c..9569c76 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,4 +1,8 @@ eureka: client: service-url: - defaultZone: http://localhost:8761/eureka/ + defaultZone: http://localhost:${EUREKA_PORT:8761}/eureka/ + +cors: + allowed-origins: + - http://localhost:3000 \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index aa23265..cce8780 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -5,4 +5,11 @@ eureka: spring: cloud: discovery: - enabled: false \ No newline at end of file + enabled: false + +jwt: + secret: test-secret-key-test-secret-key-test-secret-key + +cors: + allowed-origins: + - http://localhost:3000 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8eb5278..07db298 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -61,3 +61,9 @@ spring: server: port: 19000 + +jwt: + + +cors: + allowed-origins: [] \ No newline at end of file From 0f993a4cdbf419d2fbcfc499495e4b67386246a2 Mon Sep 17 00:00:00 2001 From: Sehi55 Date: Thu, 30 Apr 2026 05:06:06 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20cors=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/config/CorsConfig.java | 41 +++++++++++++++++++ .../infrastructure/config/CorsProperties.java | 10 +++++ 2 files changed, 51 insertions(+) create mode 100644 src/main/java/com/michelet/gateway/infrastructure/config/CorsConfig.java create mode 100644 src/main/java/com/michelet/gateway/infrastructure/config/CorsProperties.java diff --git a/src/main/java/com/michelet/gateway/infrastructure/config/CorsConfig.java b/src/main/java/com/michelet/gateway/infrastructure/config/CorsConfig.java new file mode 100644 index 0000000..6f2e8a7 --- /dev/null +++ b/src/main/java/com/michelet/gateway/infrastructure/config/CorsConfig.java @@ -0,0 +1,41 @@ +package com.michelet.gateway.infrastructure.config; + +import java.util.List; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableConfigurationProperties(CorsProperties.class) +public class CorsConfig { + private final CorsProperties corsProperties; + + public CorsConfig(CorsProperties corsProperties) { + this.corsProperties = corsProperties; + } + + @Bean + public CorsWebFilter corsWebFilter(){ + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(corsProperties.allowedOrigins()); + config.setAllowedMethods(List.of( + HttpMethod.GET.name(), + HttpMethod.DELETE.name(), + HttpMethod.POST.name(), + HttpMethod.PUT.name(), + HttpMethod.PATCH.name(), + HttpMethod.OPTIONS.name() + )); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); //쿠키에 refreshToken + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return new CorsWebFilter(source); + } +} diff --git a/src/main/java/com/michelet/gateway/infrastructure/config/CorsProperties.java b/src/main/java/com/michelet/gateway/infrastructure/config/CorsProperties.java new file mode 100644 index 0000000..1c2d1f5 --- /dev/null +++ b/src/main/java/com/michelet/gateway/infrastructure/config/CorsProperties.java @@ -0,0 +1,10 @@ +package com.michelet.gateway.infrastructure.config; + +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "cors") +public record CorsProperties( + List allowedOrigins +) { +} From 3bbdb949adc73fa799be49784896974607b365f0 Mon Sep 17 00:00:00 2001 From: Sehi55 Date: Thu, 30 Apr 2026 05:06:27 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20security=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/config/SecurityConfig.java | 51 +++++++++++++++++++ .../security/GatewayRoleConverter.java | 22 ++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/main/java/com/michelet/gateway/infrastructure/config/SecurityConfig.java create mode 100644 src/main/java/com/michelet/gateway/infrastructure/security/GatewayRoleConverter.java diff --git a/src/main/java/com/michelet/gateway/infrastructure/config/SecurityConfig.java b/src/main/java/com/michelet/gateway/infrastructure/config/SecurityConfig.java new file mode 100644 index 0000000..00447b4 --- /dev/null +++ b/src/main/java/com/michelet/gateway/infrastructure/config/SecurityConfig.java @@ -0,0 +1,51 @@ +package com.michelet.gateway.infrastructure.config; + +import com.michelet.gateway.infrastructure.security.GatewayRoleConverter; +import java.nio.charset.StandardCharsets; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter; +import org.springframework.security.web.server.SecurityWebFilterChain; + +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, GatewayRoleConverter roleConverter){ + ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(roleConverter); + return http + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .authorizeExchange( + exchanges-> exchanges.pathMatchers( + "/api/*/auth/login", + "/api/*/auth/reissue", + "/api/*/users/signup" + ).permitAll() + .pathMatchers("/api/*/admin/**").hasRole("MASTER") + .anyExchange().authenticated() + + ) + .oauth2ResourceServer(oauth2-> oauth2.jwt( + jwtSpec -> jwtSpec.jwtAuthenticationConverter(jwtAuthenticationConverter) + )).build(); + } + + @Bean + public ReactiveJwtDecoder reactiveJwtDecoder(@Value("${jwt.secret}") String secret){ + SecretKey secretKey = new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), + "HmacSHA256" + ); + return NimbusReactiveJwtDecoder.withSecretKey(secretKey).build(); + } + + +} diff --git a/src/main/java/com/michelet/gateway/infrastructure/security/GatewayRoleConverter.java b/src/main/java/com/michelet/gateway/infrastructure/security/GatewayRoleConverter.java new file mode 100644 index 0000000..90b5985 --- /dev/null +++ b/src/main/java/com/michelet/gateway/infrastructure/security/GatewayRoleConverter.java @@ -0,0 +1,22 @@ +package com.michelet.gateway.infrastructure.security; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; + +@Component +public class GatewayRoleConverter implements Converter> { + + @Override + public Flux convert(Jwt jwt) { + String role = jwt.getClaimAsString("role"); + + if(role == null || role.isBlank()) + return Flux.empty(); + + return Flux.just(new SimpleGrantedAuthority("ROLE_" +role)); + } +} From 46d353b521652f1354ae466f32582aa1d6f5841d Mon Sep 17 00:00:00 2001 From: Sehi55 Date: Thu, 30 Apr 2026 05:06:36 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/JwtAuthenticationFilter.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/main/java/com/michelet/gateway/infrastructure/security/JwtAuthenticationFilter.java diff --git a/src/main/java/com/michelet/gateway/infrastructure/security/JwtAuthenticationFilter.java b/src/main/java/com/michelet/gateway/infrastructure/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..0f88700 --- /dev/null +++ b/src/main/java/com/michelet/gateway/infrastructure/security/JwtAuthenticationFilter.java @@ -0,0 +1,56 @@ +package com.michelet.gateway.infrastructure.security; + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@Component +public class JwtAuthenticationFilter implements GlobalFilter, Ordered { + private static final String USER_ID_HEADER = "X-User-Id"; + private static final String USER_ROLE_HEADER = "X-User-Role"; + + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + return ReactiveSecurityContextHolder.getContext() + .map(securityContext -> securityContext.getAuthentication()) + .flatMap(authentication -> addHeaders(authentication, exchange,chain)) + .switchIfEmpty(chain.filter(exchange)) + .onErrorResume(ex-> chain.filter(exchange)); + } + + public Mono addHeaders( + Authentication authentication, + ServerWebExchange exchange, + GatewayFilterChain chain + ){ + if(!(authentication instanceof JwtAuthenticationToken)) + return chain.filter(exchange); + Jwt jwt = ((JwtAuthenticationToken) authentication).getToken(); + + ServerHttpRequest mutatedRequest = exchange.getRequest() + .mutate() + .header(USER_ID_HEADER, normalize(jwt.getSubject())) + .header(USER_ROLE_HEADER, normalize(jwt.getClaimAsString("role"))) + .build(); + + return chain.filter(exchange.mutate().request(mutatedRequest).build()); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 5; + } + + private String normalize(String value){ + return value == null ? "" : value; + } +} From 1fa9a69055e4880aa4a0c6529fcaf338f9e74521 Mon Sep 17 00:00:00 2001 From: Sehi55 Date: Thu, 30 Apr 2026 10:24:54 +0900 Subject: [PATCH 6/7] fix: harden gateway auth header handling --- .../security/JwtAuthenticationFilter.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/michelet/gateway/infrastructure/security/JwtAuthenticationFilter.java b/src/main/java/com/michelet/gateway/infrastructure/security/JwtAuthenticationFilter.java index 0f88700..43f8762 100644 --- a/src/main/java/com/michelet/gateway/infrastructure/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/michelet/gateway/infrastructure/security/JwtAuthenticationFilter.java @@ -21,10 +21,14 @@ public class JwtAuthenticationFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { return ReactiveSecurityContextHolder.getContext() - .map(securityContext -> securityContext.getAuthentication()) - .flatMap(authentication -> addHeaders(authentication, exchange,chain)) - .switchIfEmpty(chain.filter(exchange)) - .onErrorResume(ex-> chain.filter(exchange)); + .flatMap(securityContext -> { + Authentication authentication = securityContext.getAuthentication(); + if(authentication == null){ + return chain.filter(exchange); + } + return addHeaders(authentication,exchange,chain); + }) + .switchIfEmpty(chain.filter(exchange)); } public Mono addHeaders( @@ -38,8 +42,22 @@ public Mono addHeaders( ServerHttpRequest mutatedRequest = exchange.getRequest() .mutate() - .header(USER_ID_HEADER, normalize(jwt.getSubject())) - .header(USER_ROLE_HEADER, normalize(jwt.getClaimAsString("role"))) + .headers( + httpHeaders -> { + httpHeaders.remove(USER_ID_HEADER); + httpHeaders.remove(USER_ROLE_HEADER); + + String userId = jwt.getSubject(); + if (userId != null && !userId.isBlank()) { + httpHeaders.set(USER_ID_HEADER, userId); + } + + String role = jwt.getClaimAsString("role"); + if (role != null && !role.isBlank()) { + httpHeaders.set(USER_ROLE_HEADER, role); + } + } + ) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); @@ -50,7 +68,4 @@ public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 5; } - private String normalize(String value){ - return value == null ? "" : value; - } } From 0d18656443ddc64deadea347a69c3e9309f7bf17 Mon Sep 17 00:00:00 2001 From: Sehi55 Date: Thu, 30 Apr 2026 10:26:06 +0900 Subject: [PATCH 7/7] fix: add jwt.secret --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 07db298..c66f354 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -63,7 +63,7 @@ server: port: 19000 jwt: - + secret: ${JWT_SECRET} cors: allowed-origins: [] \ No newline at end of file