MSA Shopping Mall - 6(API Gateway)

김원기·2025년 3월 18일

MSA Shopping Mall

목록 보기
7/13

이번 포스팅은 API Gateway 부분을 구현할 차례다.

지난 포스팅에서 User와 Auth간의 소통을 구현했었는데
별개의 서버에서 API를 불러다 사용하는 방식으로 구현했었다.

그렇지만 MSA는 서버간의 직접 소통을 권하지는 않는다.

이 문제를 해결하기 위해서 API Gateway를 설정하고 모든 서비스는 API Gateway를 통하여 소통하도록 구현해야 한다.

JwtUtil

먼저 gateway에 JwtUtil을 구현한다.

Auth-Service 에서 받아온 로그인을 위한 인증 토큰을 사용하기 위해 필요한 구성이다.

@Component
public class JwtUtil {

    // TODO : Secret Key 환경 변수로 수정
    private final String SECRET_KEY = "2~~";

    public String extractEmail(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                    .setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

지금은 간단한 구현이기 때문에 SECRET_KEY 자체를 하드코딩 해두었다.

그 다음으로는 정보 추출과 토큰 검증이다.

JwtAuthenticationFilter

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter implements WebFilter {
    private final JwtUtil jwtUtil;
    private static final List<String> EXCLUDED_PATHS = List.of("/v1/auth/login", "/v1/users/email", "/v1/users/signup");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        if (EXCLUDED_PATHS.contains(path)) {
            return chain.filter(exchange);
        }

        String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return onError(exchange, "Missing or Invalid Authorization Header", HttpStatus.UNAUTHORIZED);
        }

        String token = authHeader.substring(7);
        if (!jwtUtil.validateToken(token)) {
            return onError(exchange, "Invalid JWT Token", HttpStatus.UNAUTHORIZED);
        }

        // userId 추출
        String userId = jwtUtil.extractEmail(token);

        // userId를 헤더에 추가해서 downstream service로 넘겨줌
        ServerWebExchange mutatedExchange = exchange.mutate()
                .request(builder -> builder.header("X-User-Id", userId))
                .build();

        return chain.filter(mutatedExchange);
    }

    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus status) {
        exchange.getResponse().setStatusCode(status);
        return exchange.getResponse().setComplete();
    }
}

전체 코드는 이러하며 각 코드가 하는 일을 조금 살펴보도록 하겠다.

WebFilter

일단 MSA 관점에서 API Gateway 레이어에서 인증 처리는 대표적인 패턴이다.

WebFilter는 Spring WebFlux의 비동기 논블로킹 필터로서, Gateway의 필터 체인에서 모든 요청 전처리 역할을 수행한다.

이 말은 다음과 같이 정리할 수 있다.

  • servlet filter가 아닌 Reactive 환경에 맞춰 설계된 필터
  • 비동기 스트림에서 효율적인 성능 제공
  • ServerWebExchange 로 요청과 응답을 통째로 다룰 수 있어 헤더 수정, Body 조작 등 유연성 확보
  • API Gateway 레이어에서 권한 체크, 토큰 파싱, 인증 실패 차단 등 전방위 컨트롤 가능

WebFlux

https://adjh54.tistory.com/232

ServerWebExchange

ServerWebExchange는 불변 객체로 mutate로 복제 후 헤더에 X-User-Id라는 헤더를 추가하여
다른 서비스로 넘겨주는 방식이다.

ServerWebExchange mutatedExchange = exchange.mutate()
    .request(builder -> builder.header("X-User-Id", userId))
    .build();

Security Config

@Configuration
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) throws Exception {
        http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .authorizeExchange(exchanges -> exchanges
                        .pathMatchers("/v1/auth/**").permitAll()
                        .pathMatchers("/v1/users/**").permitAll()
                        .pathMatchers("/logout").permitAll()
                        .pathMatchers("/error").permitAll()
                        .anyExchange().authenticated()
                )
                .addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);

        return http.build();
    }
}

평소에 사용하던 Security Config 와 비슷하지만
authorizeExchange 라던가 pathMatchers 처럼 경로를 허용하는 방식등 기존의
Web MVC 패턴이 아닌 경로 형태로 진행된다.

위와 같이 구현하면 다른 서비스에서도 인증이 가능하다.

보안적인 주의점

일단 X-User-Id 헤더와 같은 경우 Client에서의 직접적인 입력을 반드시 막아야 한다.

Gateway에서 입력해야 인증객체가 설정되는 것이기 때문에 외부에서 입력할 경우 정상적으로 작동하지 않을 가능성이 높으며 탈취와 같은 위험이 존재할 수 있다.

끝!

이번 포스팅은 조금 공부가 더 필요할 것 같기 때문에 다음 포스팅에서는 WebFlux, Mono등 조금 더 용어나 그 외의 주요 특징들을 다루는 글을 적도록 하겠다.

profile
혼자 공부하는 블로그라 부족함이 많아요 https://www.notion.so/18067a27ac7e4f4790dde645fb3bf3d3?pvs=4

0개의 댓글