Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ dependencies {
implementation('org.pgsg:common:0.3.2-SNAPSHOT') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-data-jpa'
exclude group: 'com.querydsl', module: 'querydsl-jpa'
exclude group: 'org.springdoc', module: 'springdoc-openapi-starter-webmvc-ui'
exclude group: 'org.springframework.cloud', module: 'spring-cloud-starter-openfeign'
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-web'
}

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway-server-webmvc'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway-server-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'com.github.ben-manes.caffeine:caffeine'

implementation 'io.micrometer:micrometer-tracing-bridge-brave'

Expand All @@ -51,6 +56,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.cloud:spring-cloud-contract-wiremock'
testCompileOnly 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'
Expand Down
2 changes: 0 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ services:
- "8090:8090"
env_file:
- .env.runtime
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka-server:8761/eureka/
networks:
- pgsg-network

Expand Down
2 changes: 0 additions & 2 deletions src/main/java/org/pgsg/gateway/GatewayApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@ImportAutoConfiguration(exclude = AppCtx.class)
@Import(GatewayAppCtx.class)
@EnableFeignClients
public class GatewayApplication {

public static void main(String[] args) {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/pgsg/gateway/auth/AuthProvider.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.pgsg.gateway.auth;

import reactor.core.publisher.Mono;

public interface AuthProvider {

boolean verifyToken(String accessToken);
Mono<Boolean> verifyToken(String accessToken);
}
71 changes: 28 additions & 43 deletions src/main/java/org/pgsg/gateway/auth/AuthProviderImpl.java
Original file line number Diff line number Diff line change
@@ -1,59 +1,44 @@
package org.pgsg.gateway.auth;

import lombok.RequiredArgsConstructor;
import org.pgsg.common.response.CommonResponse;
import org.pgsg.gateway.feign.AuthClient;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.pgsg.gateway.client.AuthClient;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthProviderImpl implements AuthProvider {

private static final long CACHE_TTL = 30 * 1000; // 캐시 유지 시간: 30초
private static final int MAX_CACHE_SIZE = 10000;

// 간단한 로컬 캐시 (토큰별 검증 결과 저장)
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
private final Cache<String, Boolean> tokenCache;
private final AuthClient authClient;

@Override
public boolean verifyToken(String accessToken) {
CacheEntry entry = cache.get(accessToken);

// 캐시가 유효하면 바로 반환
if (entry != null && !entry.isExpired()) {
return entry.result;
}

// 캐시가 없거나 만료되었으면 Feign 호출
CommonResponse<AuthDto.TokenVerifyData> response = authClient.verifyToken(new AuthDto.TokenVerifyRequest(accessToken));

// 결과 추출 (success 가 true 이고 isVerifiedToken 이 true 인 경우에만 성공)
boolean result = response != null && response.success() && response.data() != null && response.data().isVerifiedToken();
if (cache.size() >= MAX_CACHE_SIZE) {
cleanupCache();
if (cache.size() >= MAX_CACHE_SIZE) {
cache.clear();
}
}
cache.put(accessToken, new CacheEntry(result, System.currentTimeMillis() + CACHE_TTL));

cleanupCache();

return result;
public AuthProviderImpl(AuthClient authClient) {
this.authClient = authClient;
this.tokenCache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
}

// 만료된 캐시를 가끔 정리 (메모리 누수 방지)
private void cleanupCache() {
cache.entrySet().removeIf(e -> e.getValue().isExpired());
}
@Override
public Mono<Boolean> verifyToken(String accessToken) {
Boolean cachedResult = tokenCache.getIfPresent(accessToken);

private record CacheEntry(boolean result, long expiryTime) {
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
if (cachedResult != null) {
return Mono.just(cachedResult);
}

return authClient.verifyToken(new AuthDto.TokenVerifyRequest(accessToken))
.map(response -> response != null
&& response.success()
&& response.data() != null
&& response.data().isVerifiedToken())
.doOnNext(result -> tokenCache.put(accessToken, result))
.onErrorReturn(false);
}
}

28 changes: 28 additions & 0 deletions src/main/java/org/pgsg/gateway/client/AuthClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.pgsg.gateway.client;

import org.pgsg.common.response.CommonResponse;
import org.pgsg.gateway.auth.AuthDto;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

//@FeignClient(name = "user-service", fallbackFactory = AuthClientFallbackFactory.class)
@Component
public class AuthClient {

private final WebClient webClient;

public AuthClient(WebClient.Builder builder) {
this.webClient = builder.baseUrl("lb://user-service").build();
}

public Mono<CommonResponse<AuthDto.TokenVerifyData>> verifyToken(AuthDto.TokenVerifyRequest request) {
return webClient.post()
.uri("/internal/v1/auth/verify")
.bodyValue(request)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<CommonResponse<AuthDto.TokenVerifyData>>() {})
.onErrorReturn(new CommonResponse<>(false, "인증 서비스 장애", new AuthDto.TokenVerifyData(false), null));
}
}
62 changes: 5 additions & 57 deletions src/main/java/org/pgsg/gateway/config/GatewayAppCtx.java
Original file line number Diff line number Diff line change
@@ -1,75 +1,23 @@
package org.pgsg.gateway.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.pgsg.common.exception.ErrorConfigProperties;
import org.pgsg.common.exception.GlobalExceptionAdvice;
import org.pgsg.common.exception.GlobalExceptionAdviceImpl;
import org.pgsg.common.filter.MdcLoggingFilter;
import org.pgsg.common.response.CommonResponseAdvice;
import org.pgsg.config.feign.FeignConfig;
import org.pgsg.config.json.JsonConfig;
import org.pgsg.config.security.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
@Import({
FeignConfig.class,
JsonConfig.class,
ErrorConfigProperties.class
})
public class GatewayAppCtx {

@Bean
public LoginFilter loginFilter(@Lazy @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
return new LoginFilter(resolver);
}

@Bean
public CustomAuthenticationEntryPoint customAuthenticationEntryPoint(
ObjectMapper objectMapper, ErrorConfigProperties errorConfigProperties) {
return new CustomAuthenticationEntryPoint(objectMapper, errorConfigProperties);
}

@Bean
public CustomAccessDeniedHandler accessDeniedHandler(
ObjectMapper objectMapper, ErrorConfigProperties errorConfigProperties) {
return new CustomAccessDeniedHandler(objectMapper, errorConfigProperties);
}

@Bean
@ConditionalOnMissingBean(SecurityConfig.class)
public SecurityConfig securityConfig(
LoginFilter loginFilter,
CustomAuthenticationEntryPoint customAuthenticationEntryPoint,
CustomAccessDeniedHandler accessDeniedHandler) {
return new SecurityConfigImpl(loginFilter, customAuthenticationEntryPoint, accessDeniedHandler);
}

@Bean
@ConditionalOnMissingBean(GlobalExceptionAdvice.class)
public GlobalExceptionAdvice globalExceptionAdvice(ErrorConfigProperties errorConfigProperties) {
return new GlobalExceptionAdviceImpl(errorConfigProperties);
}

@Bean
public CommonResponseAdvice commonResponseAdvice() {
return new CommonResponseAdvice();
}

@Bean
public FilterRegistrationBean<MdcLoggingFilter> mdcLoggingFilter() {
FilterRegistrationBean<MdcLoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new MdcLoggingFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
28 changes: 12 additions & 16 deletions src/main/java/org/pgsg/gateway/config/GatewaySecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
package org.pgsg.gateway.config;

import lombok.RequiredArgsConstructor;
import org.pgsg.config.security.SecurityConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
@EnableWebSecurity
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class GatewaySecurityConfig implements SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session
-> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
);
return http.build();
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(auth -> auth.anyExchange().permitAll())
.build();
}
}
}
14 changes: 0 additions & 14 deletions src/main/java/org/pgsg/gateway/feign/AuthClient.java

This file was deleted.

This file was deleted.

Loading
Loading