Spring Cloud Gateway에서 데이터 검증 문제 해결하기 (feat. GatewayFilter)

Lord·2025년 1월 6일

Problem Solving Skills

목록 보기
7/17
post-thumbnail

Spring Cloud Gateway를 활용하여 Cloud Native Application을 개발하면서 MSA(Microservices Architecture) 구조를 채택해 여러 서비스의 백엔드 개발을 맡게 되었다. 인증 및 인가에 대한 모든 과정은 Edge Service에서 진행하도록 설계했고, 모든 클라이언트 요청은 Edge Service를 통해 전달되도록 API Gateway 역할을 수행하게 하였다.

처음에는 모든 요청이 Edge Service를 통해 검증되기 때문에 보안 문제가 없을 것이라고 판단했다. 하지만 프로젝트를 진행하며 해당 사용자가 다른 사용자의 정보를 조회할 가능성이 있다는 것을 알게 되었다. 백엔드에서는 GET 요청에 대해 클라이언트로부터 멤버의 아이디 값을 파라미터로 전달받아 처리하게 된다. 이 아이디 값은 Access Token의 Claim에서 추출한 뒤 프론트엔드에서 전달된다. 이를 통해 요청은 Access Token의 주인에 대한 데이터만 반환하도록 설계되었다.

문제는 Edge Service에서 Token에 대한 검증이 이루어진 후, 다른 서비스로 요청이 전달될 때 발생한다. 다른 서비스에서는 Security Context나 추가적인 검증 없이 Edge Service에서 전달받은 요청을 처리하게 되므로, 데이터 검증 없이 정보를 반환하는 보안적인 약점이 드러났다. 즉, 특정 사용자가 다른 사용자의 데이터를 요청할 가능성이 열려 있던 것이었다.

이를 해결하기 위해, 각 마이크로서비스에서도 데이터에 대한 검증과 Access Token의 유효성을 점검할 필요가 있다고 판단했다. 이를 위해 Edge Service에서 요청 헤더에 사용자 ID를 포함하는 Custom Header를 추가하여, 요청의 출처를 명확히 하고 데이터 접근의 보안성을 강화하는 방식을 적용하였다. 이번 글에서는 문제의 원인과 이를 해결한 과정, 그리고 관련 코드에 대해 자세히 설명한다.


문제 정의

문제 상황

인증된 사용자가 자신의 토큰으로 다른 사용자의 냉장고 정보를 조회할 수 있는 보안 취약점이 발견되었다. 이는 사용자 간 데이터 격리가 이루어지지 않았음을 의미하며, 민감한 데이터가 부적절하게 노출될 위험성을 가지고 있다. 특히, 데이터베이스 수준에서 검증되지 않은 요청이 허용되면서 사용자의 개인정보가 보호되지 않는 문제가 발생했다.

예를 들어, 사용자가 유효한 인증 토큰을 사용하여 자신의 데이터뿐만 아니라 다른 사용자의 데이터를 요청할 경우, 요청 검증 없이 데이터가 반환되는 상황이었다. 이는 기본적인 보안 원칙인 최소 권한 접근을 위반하는 문제였다.

물론, 사용자가 API를 직접 요청하는 일은 없겠지만 백엔드 개발자 입장에서 이는 매우 중요하게 생각해야할 부분이다.

원인 분석

Gateway에서 요청을 다른 서비스로 전달할 때, 요청의 출처(사용자 정보)를 확인하는 데이터베이스 검증 로직이 없었다. 이로 인해 인증 토큰만으로 요청을 처리하는 경우, 사용자가 권한이 없는 데이터에 접근할 수 있었다. 이러한 구조적 문제는 보안 사고로 이어질 가능성을 가지고 있었으며, 사용자 신뢰도에도 부정적인 영향을 미칠 수 있었다.


해결 방안

문제를 해결하기 위해, Edge Service에서 요청 헤더에 사용자 ID(memberId)를 추가하는 필터를 구현했다. 이 필터는 요청이 어느 사용자로부터 온 것인지 명확히 하기 위해, Spring Security의 인증 정보를 활용하여 사용자 ID를 헤더에 추가한다. 이를 통해 각 서비스는 요청의 출처를 명확히 확인하고 적절히 처리할 수 있다.

추가로, 필터 적용 대상 경로를 설정하여 불필요한 경로에 대해 필터를 사용하여 우회하도록 구성했다. 필터는 성능 최적화와 필요 없는 검증 작업을 줄이기 위해 사용하였다. 이렇게 필터를 도입함으로써 시스템의 복잡성을 증가시키지 않으면서도 보안을 강화할 수 있었다.


구현 세부 사항

1. 사용자 ID를 추가하는 Filter 주요 코드

필터를 통해 사용자 ID를 요청 헤더에 추가하여 보안 문제를 해결했다.

@Override
public void doFilter(jakarta.servlet.ServletRequest request,
                     jakarta.servlet.ServletResponse response,
                     FilterChain chain) throws IOException, ServletException {

    HttpServletRequest httpRequest = (HttpServletRequest) request;
    String requestURI = httpRequest.getRequestURI();

    if (shouldNotFilter(requestURI)) {
        chain.doFilter(request, response);
        return;
    }

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null && authentication.isAuthenticated()) {
        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        Long memberId = principalDetails.getId();
        MutableHttpServletRequest mutableRequest = new MutableHttpServletRequest(httpRequest);
        mutableRequest.addHeader("X-Member-Id", String.valueOf(memberId));
        chain.doFilter(mutableRequest, response);
        return;
    }

    chain.doFilter(request, response);
}

인증 정보에서 사용자 ID를 추출하고, 이를 요청 헤더에 X-Member-Id라는 이름의 헤더로 추가한 뒤 다음 필터로 전달한다. 즉, 다른 서비스에서는 액세스 토큰 대신 X-member-Id로 데이터 검증을 실시하는 것이다. 특정 경로에 대해 필터를 제외할 수 있는 로직도 포함되어 있다. 이를 통해 필요하지 않은 요청에 대해 불필요한 필터 작업을 수행하지 않는다.

다른 마이크로 서비스에서는 아래와 같이 헤더에서 멤버 아이디를 추출하고 데이터 검증을 실시한다.

@Override
@Transactional(readOnly = true)
public RefrigeratorWithIngredientsResponse getRefrigerator(Long memberId, Long authenticatedMemberId) {
    validationMember(memberId, authenticatedMemberId);
    Refrigerator refrigerator = refrigeratorRepository.findByMemberId(memberId).orElseThrow(() -> new RefrigeratorException(RefrigeratorExceptionType.REFRIGERATOR_NOT_FOUND));
    return RefrigeratorWithIngredientsResponse.of(refrigerator);
    }

private void validationMember(Long memberId, Long authenticatedMemberId) {
        if (!Objects.equals(memberId, authenticatedMemberId)) {
            throw new CustomAuthenticationException(CustomAuthenticationExceptionType.AUTHENTICATION_DENIED);
        }
    }

요청 파라미터에서 memberId를 가져오고 해당 값과 요청 헤더에 들어있는 X-Member- Id의 값이 일치하는지 검증한다. 이를 통해 실제 데이터를 원하는 멤버와 가져오려는 데이터의 멤버 아이디가 일치하는지 확인할 수 있다.

2. FilterRegistrationBean을 사용한 필터 등록

FilterRegistrationBean을 사용해 필터를 등록하였다. Edge service에서 다른 서비스들로 요청이 전달되기 직전에 거치는 필터로 사용되도록 Spring Gateway Filter로 등록하였다.

@Bean
public FilterRegistrationBean<AddMemberIdHeaderFilter> addMemberIdHeaderFilter() {
    FilterRegistrationBean<AddMemberIdHeaderFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new AddMemberIdHeaderFilter());
    registrationBean.addUrlPatterns("/*");
    registrationBean.setOrder(2);
    return registrationBean;
}

FilterRegistrationBean을 사용하면 필터의 실행 순서를 세밀하게 제어할 수 있으며, 특정 URL 패턴에만 필터를 적용할 수도 있다. 위 코드는 모든 경로(/*)에 필터를 적용하며, 실행 순서를 2로 설정한 예제다. 추후 다른 포스팅에서 설명할 Rate Limiter Filter를 Order 1로 지정하였기 때문이다.


적용 결과

최종 프로세스

최종적으로 아래와 같은 프로세스를 거치게 된다.

특정 API 요청 (재료 관리 서비스나 리뷰 관리 서비스) -> 요청이 Edge Service로 라우팅 -> Edge Service에서 Access Token을 검증하고 memberId를 추출해서 X-Member-Id 헤더를 추가 -> 그대로 요청한 서비스에 전달 (라우팅) -> 해당 서비스에서 X-Member-Id 헤더를 이용해서 데이터 검증

  1. 요청 검증 강화: 요청 헤더에 사용자 ID를 추가하여 다른 서비스에서도 요청 출처를 명확히 검증할 수 있게 되었다. 이는 데이터 접근의 정확성을 보장한다.
  2. 보안 강화: 인증된 사용자만 자신의 데이터에 접근할 수 있도록 보장되었다. 이를 통해 사용자 간 데이터 격리가 확실히 이루어졌다.
  3. 성능 최적화: 필터를 제외할 경로를 명시적으로 설정하여 필요하지 않은 경로에서는 필터를 실행하지 않도록 하였다. 이는 시스템 자원의 효율적 사용과 응답 속도 개선에 기여했다.
  4. 운영 안정성 강화: 보안 문제가 사전에 해결됨으로써 이용하는 사용자의 신뢰도를 높이고, 운영 환경에서 발생할 수 있는 잠재적 리스크를 줄일 수 있었다.

이러한 개선 사항은 서비스의 신뢰성을 높이고, 장기적으로 유지보수 부담을 줄이는 데 기여했다. 또한, 팀원들에게 여러 칭찬을 들을 수 있었다. 미처 생각하지 못한 부분에 대해서 또 고민하고 고민하여 미리 예상하고 해결하는 모습에서 좋게 봐주신 것 같다.


결론

API Gateway와 Edge Service는 요청의 인증 및 데이터 검증에서 중요한 역할을 한다. 이번 이슈에서는 요청 헤더에 사용자 ID를 추가하는 방식으로 보안 문제를 해결했다. 이러한 방식은 보안성을 높일 뿐만 아니라, 서비스 간 통신의 신뢰성을 강화하는 데 큰 도움이 된다고 생각한다.

이번 트러블슈팅을 통해 데이터 검증의 중요성을 다시 한 번 실감할 수 있었다. 앞으로도 이러한 문제를 사전에 방지하기 위해, 설계 단계에서부터 보안을 고려한 구조를 적용할 예정이다. 추가적으로, 유사한 문제를 방지하기 위해 다른 서비스 간 통신에서도 이와 같은 검증 로직을 확장하여 적용할 계획이다.

나아가, 이번 해결책은 단순히 문제 해결에 그치지 않고, 서비스 전반의 보안 체계를 재점검하고 개선하는 계기가 되었다. 이를 통해 향후 발생할 수 있는 유사한 문제를 최소화하고, 서비스 운영의 안정성을 지속적으로 높여나가는 스킬을 꾸준히 향상시킬 것이다.

profile
다재다능한 Backend 개발자에 도전하는 개발자

0개의 댓글