본문 바로가기
💻 뚝딱뚝딱/북북클럽

[개발일지 #045] Spring Cloud Gateway를 이용한 인증 구조 통합하기

by 뚜루리 2025. 6. 18.
728x90
320x100

🎯 오늘의 개발 내용 (요약)

  • Spring Cloud Gateway를 이용한 인증 구조 통합하기

 


✅ Spring Cloud Gateway란?

API 요청의 진입점(Gateway)을 담당하는 경량 프록시 서버임.
API 요청 흐름을 제어하거나, 공통 처리를 전담하는 데 사용되며, 그 중에서도 GlobalFilter 를 통해 모든 요청에 공통 로직(JWT 인증 등)을 적용할 수 있음.

 

 도입이유

 

기존에는 각 서비스(user, post 등)가 직접 JWT 토큰을 검증했기 때문에 중복된 인증 로직이 생기고 코드가 분산되어 관리가 어렵고 마이크로서비스가 늘어날수록 유지보수가 힘들어지는 구조임.
그래서! JWT 인증 책임을 Gateway로 통합하여 코드 중복 제거 인증 흐름 중앙 집중화 구조를 더 단순하고 안정적으로 리팩토링하기 위해 도입함.

 

 도입 방법 (요약)

1. bbc-gateway 모듈 생성

 

  • Spring Cloud Gateway 의존성 추가
  • 사용자 요청을 user/post-service로 라우팅 설정

 

 

2. JWT 인증 필터 구현 (JwtAuthFilter)

package com.bookbookclub.bbcgateway.filter;

import com.bookbookclub.bbcgateway.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * JWT 인증 필터 - 모든 요청에 대해 토큰 검증
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthFilter implements GlobalFilter, Ordered {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public int getOrder() {
        return -1; // 가장 먼저 실행
    }

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

        // 화이트리스트 (인증 제외 경로)
        if (isWhiteList(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 (!jwtTokenProvider.validateToken(token)) {
            return onError(exchange, "Invalid or expired token", HttpStatus.UNAUTHORIZED);
        }

        Long userId = jwtTokenProvider.getUserId(token);
        ServerWebExchange mutatedExchange = exchange.mutate()
                .request(builder -> builder.header("X-USER-ID", userId.toString()))
                .build();

        return chain.filter(mutatedExchange);
    }

    private boolean isWhiteList(String path) {
        return path.startsWith("/api/auth") || path.startsWith("/api/public");
    }

    private Mono<Void> onError(ServerWebExchange exchange, String message, HttpStatus status) {
        log.warn("[JwtAuthFilter] 인증 실패: {}", message);
        exchange.getResponse().setStatusCode(status);
        return exchange.getResponse().setComplete();
    }
}
  • 모든 요청에 대해 Authorization: Bearer {token} 검증
  • 검증 성공 시 → 토큰에서 userId 추출
  • userId를 X-USER-ID 헤더로 내부 서비스에 전달

 

3. 내부 서비스 리팩토링 (user/post)

package com.bookbookclub.bbc_post_service.like.controller;

import com.bookbookclub.bbc_post_service.like.service.LikeService;
import com.bookbookclub.common.response.ApiResponse;
import com.bookbookclub.common.security.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/feeds/{feedId}/likes")
public class LikeController {

    private final LikeService likeService;

    /**
     * 좋아요 등록
     */
    @PostMapping
    public ApiResponse<Void> like(@PathVariable Long feedId,
                                  @RequestHeader("X-USER-ID") Long userId) {
        likeService.like(userId, feedId);
        return ApiResponse.success("좋아요가 등록되었습니다.");
    }
	//코드생략//
}
  • 기존 JwtAuthenticationFilter 및 JwtUtil 제거(post-service)
  • @RequestHeader("X-USER-ID") 방식으로 사용자 식별
  • Spring Security는 인가 로직만 담당하도록 단순화

 

 

4. JWT secret 키 통일

/**
 * JWT 토큰 유효성 검증 및 사용자 ID 추출
 */
@Component
public class JwtTokenProvider {

    @Value("${jwt.secret}")
    private String secretKey;

    /**
     * 시크릿 키를 기반으로 서명용 Key 객체 생성
     */
    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey); // secretKey는 Base64 인코딩된 문자열이어야 함
        return Keys.hmacShaKeyFor(keyBytes);
    }

    //코드생략//
}
  • JWT 생성 시 사용하는 secretKey를 Base64 인코딩 후 환경변수로 통일
  • Gateway와 user-service가 동일한 방식으로 토큰을 생성/검증할 수 있도록 조정

 

 

728x90
320x100