본문 바로가기
🔬아키텍처/- Spring Cloud for MSA

[Spring Cloud Gateway] Custom Filter 로 간단한 Authentiction 필터 만들고 인증 처리하기

by Wonit 2021. 4. 25.

해당 글은 Spring Cloud Gateway 의 Built-in Route로 Predicates와 Filter 조작하기) 에 의존하는 글입니다. 실습 환경을 따라하시려면 이전 글을 확인하시길 바랍니다.

현재 글에서는 모든 인증 과정 (jwt 토큰을 발급하고 payload 에 값을 넣는 실제 구현)은 하지 않습니다. 이번 글의 목적은 Gateway 에서 Custom Filter 만들어서 토큰 검증을 수행하는 전체적인 큰 그림만 보여주려 하기 때문에 실제 인증을 구현하려 한다면 참고만 하시길 바랍니다.

목차

  • Custom Filter
  • AbstractGatewayFilterFactory 상속과 apply 재정의
  • 검증하기

Custom Filter

지난 시간까지 우리는 Spring Cloud Gateway가 기본적으로 제공하는 Built-in Predicates와 Filters를 알아보았다.

 

이번 시간에는 우리가 원하는 기능을 수행하는 Custom Filter를 만들고 Gateway Filters에 등록해보자.

 

무엇을 만들 것인가?

간단하게 말 하면 JWT 토큰 파싱을 하는 Filter를 만들 것이다.

 

Microservice 에서 인증은 Stateless 한 방식을 적용하는 것이 불가피하다.


만약 그렇지 않은 Session-Cookie 기반 인증을 사용하게 된다면 모든 서버가 하나의 DB를 바라보고 사용자의 Session과 Cookie를 관리하거나 모든 서버에서 Session을 따로 관리해야 한다.

 

하지만 Request Header에 Token String을 담아 stateless한 인증을 사용하면 위의 문제를 쉽게 해결할 수 있다.

 

그렇기에 우리는 토큰을 이용한 인증 방식을 위해 Gateway를 사용한다.

 

왜 인증을 Gateway에서 할까?

 

그 이유는 바로 Gateway의 흐름을 보면 알 수 있다.

 

위에서 본다면 당연하게 모든 요청이 Gateway로 몰리게 된다.

 

그럼 각각의 서버에서 인증을 할 필요 없이 Gateway 에서 한 번만 인증을 수행하고, 다른 서버들은 요청이 오면 인증된 순수 요청이라고 하면 어떨까?


물론 어떠한 상황에서도 모든 공격을 막을 수는 없겠지만 당장으로서는 해당 방법이 좋은것 같고 충분히 이를 구현할만 하다.

 

그럼 지금부터 커스텀 필터를 만들고 인증을 수행하는 과정을 추가해보자.

 

앞서 말했지만 현재 글에서는 모든 인증 과정, jwt 토큰을 발급하고 payload 에 값을 넣는 실제 구현은 하지 않으려 한다. 이번 글의 목적은 커스텀 필터를 만들어서 토큰 검증을 수행하는 전체적인 큰 그림만 보여주려 하기 때문에 실제 인증을 구현하려 한다면 참고만 하길 바란다.

 

CustomAuthFilter로 인증 구현하기

Spring Cloud Gateway는 Zuul 에 비동기 통신 기능이 추가된 버전이라고 생각하면 쉽다.


그래서 Blocking 구조가 아닌 Non-Blocking 구조인 Spring WebFlux 로 구현되어 있기 때문에 Filter들을 모두 Flux로 구현해야 한다.

 

토큰을 검증하는 과정은 다음과 같이 흘러갈 것이다.

 

  1. 사용자 요청
  2. Handler Mapping 이 Predicates 검사
  3. Pre Filter 에서 Request Header 에 있는 token 파싱
  4. 만약 존재하지 않는다면 401 Error Reject
  5. 존재한다면 사용자가 요청한 서버로 요청 전달

 

CustomFilter.class 생성하기

우선 사용자 정의 필터를 만들기 위해서는 Spring Cloud Gateway 가 추상화해 놓은 AbstractGatewayFilterFactory 를 상속받는다.

 

그리고 GatewayFilterFactory 를 구현할 때 우리의 로직을 넣으면 되는데, 해당 추상화 메서드를 GatewayFilter apply(Config config) override 한다.

 

@Component
public class CustomAuthFilter extends AbstractGatewayFilterFactory<CustomAuthFilter.Config> {
    public CustomAuthFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return null;
    }

    public static class Config {

    }
}

 

Config class 에서는 Configuration 속성들을 넣어주면 되는데, 다음에 있을 Global Filter에서 자세하게 이야기해보려 한다.
현재는 그냥 비어있는 클래스로 넣어주자.

 

토큰 검증 로직 추가하기

이제 토큰 검증을 할 로직을 apply 메서드에 추가하면 된다.

 

@Component
public class CustomAuthFilter extends AbstractGatewayFilterFactory<CustomAuthFilter.Config> {
    public CustomAuthFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return ((exchange, chain) -> {

        });
    }

    public static class Config {

    }
}

 

apply 메서드 안에서 첫 번째 매개변수는 ServerWebExchange 형태고 두 번째 파라미터가 GatewayFilterChain 람다 함수이다.

 

ServerHttpRequest request = exchange.getRequest(); // Pre Filter

ServerHttpResponse response = exchange.getResponse(); // Post Filter

 

exchange 의 Request를 받아오면 Pre Filter로 적용되고 Post Filter는 Response로 받아오며 된다.

 

주의해야할 점이 ServletReqeust, Response가 아닌 Spring Reactive 의 Response, Requset여야 한다.

 

토큰 검증 로직 완성하기

@Component
public class CustomAuthFilter extends AbstractGatewayFilterFactory<CustomAuthFilter.Config> {
    public CustomAuthFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return ((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            // Request Header 에 token 이 존재하지 않을 때
            if(!request.getHeaders().containsKey("token")){
                return handleUnAuthorized(exchange); // 401 Error
            }

            // Request Header 에서 token 문자열 받아오기
            List<String> token = request.getHeaders().get("token");
            String tokenString = Objects.requireNonNull(token).get(0);

            // 토큰 검증
            if(!tokenString.equals("A.B.C")) {
                return handleUnAuthorized(exchange); // 토큰이 일치하지 않을 때
            }

            return chain.filter(exchange); // 토큰이 일치할 때

        });
    }

    private Mono<Void> handleUnAuthorized(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();

        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        return response.setComplete();
    }

    public static class Config {

    }
}

application.yml에 우리가 정의한 CustomFilter 등록하기

server:
  port: 8000

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user/**
          filters:
            - RewritePath=/user/?(?<segment>.*), /$\{segment}
            - CustomAuthFilter

        - id: order-service
          uri: lb://ORDER-SERVICE
          predicates:
            - Path=/order/**
          filters:
            - RewritePath=/user/?(?<segment>.*), /$\{segment}

 

user-serivce 에 CustomAuthFilter 를 추가시켜주면 우리의 필터가 드디어 Gateway 에 들어가게 되는 것이다.

 

검증하기

우리의 필터가 잘 동작하는지 검증하기 위해서 기존에 구성하던 Service Mesh와 Microservices 를 실행시켜보자.

  • Eureka Server
  • Gateway Service
  • User Service
  • Order Service

그리고 Gateway로 user service에 GET /info 요청을 보내보자.

 

토큰 없이 요청 보내기

 

현재 요청에는 Request Header 에 token 필드가 존재하지 않기 때문에 401 에러가 발생하게 된다.

 

invalid 한 토큰을 포함한 요청 보내기

 

토큰 검증 로직에서 A.B.C 라는 토큰이 와야지만 정상 요청을 보내게 하는데, 잘못된 토큰을 보내보자.

 

 

역시 401 에러로 접근하지 못하게 된다.

 

valid 한 토큰을 포함한 요청 보내기

이번에는 Valid 한 토큰, A.B.C 값을 헤더에 포함시켜 요청을 보내보자.

 

그럼 위와같이 200 OK로 정상적으로 우리의 요청이 로드밸런싱 된 User-Service로 도착하게 된다.

 


오늘은 이렇게 Spring Cloug Gateway를 이용해서 간단한 인증 필터를 구현해보았다.

 

이런 인증은 그 어떤 서비스에서도 사용하지 않겠지만 인증을 위한 토큰 검증 흐름은 크게 다르지 않을 것이다.

 

해당 글을 보고 흐름을 파악한 뒤 각자의 프로젝트에 성공적인 결과를 기대한다.

 

댓글7

  • 이멀젼씨 2021.08.13 21:04

    우선 좋은글 감사드립니다.
    궁금한 점이 있는데 왜 필터를 user-service전에 적용시켜주는건가요?
    게이트웨이에 요청이 들어가기 전에 적용할 수 있는것 같아 질문드립니다
    답글

    • Favicon of https://wonit.tistory.com BlogIcon Wonit 2021.08.13 21:11 신고

      저도 좋은 질문 감사합니다!

      게이트웨이에 요청이 들어가기 전이라고 함은 Gateway 자체 filter 를 말씀하시는걸까요 ?

      제가 질문을 이해한 바가 맞다면 Gateway 가 돌아가는 애플리케이션에 filter 를 적용시킬 수 있다는 말씀이신데, Custom Filter 자체가 GatewayFilter 를 상속받기 때문에 저 자체가 Gateway 의 Filter 입니다.

      그리고 user-service 로 요청이 들어가기 직전에 필터를 걸어주는 이유는 다음과 같습니다.

      예를 들어 A라는 서비스와 B라는 서비스가 존재하고 Gateway 로 연결되어 있습니다. A는 인가된 사용자만 접근할 수 있고 B 는 모두에게 공개된 서비스라고 가정해볼게요

      Gateway 로 들어오는 모든 요청을 필터링을 해버린다면 모두에게 공개된 B 라는 서비스는 제대로 동작하지 않게 될 수 있습니다.

      제가 질문을 잘 이해한 것인지 모르겠네요 ㅎㅎ 더 질문할 게 있으시다면 올려주세요! 저도 현재 공부하고 있는 중이라 바로 답변해드릴게요 ㅎㅎ

  • 이멀젼씨 2021.08.13 21:43

    저는 모든 서비스에 인증이 다 필요한줄 알고있었습니다!

    답변 감사드립니다

    그나저나 제가 공부하는 책이랑 흐름이 유사한것 같아서 그런데요, 혹시 공부하시는 책이나 강의가 뭔지 알 수 있을까요?
    답글

    • Favicon of https://wonit.tistory.com BlogIcon Wonit 2021.08.13 21:45 신고

      그렇군요!
      저는 책을 보고싶지만 뭐가 좋은지 몰라서 못 보고 있고 강의를 들었었습니다!

      inflearn 에서 이대원 강사님의 Spring cloud 로 시작하는 msa 를 들었네요 ㅎㅎ

      혹시 좋은 책이 있다면 소개좀 해주실 수 있을까요?!

  • Favicon of https://emgc.tistory.com BlogIcon 이멀젼씨 2021.08.13 21:52 신고

    저는 '스프링으로 하는 마이크로 서비스 구축' 를 보고 공부중입니다. (http://www.yes24.com/Product/Goods/95593443)

    스프링으로 구축하는 마이크로서비스에 대해 얕고 넓게 구조와 흐름을 알기에 적당한 책인것 같습니다

    게이트웨이와 유레카서버 설정등에 대해서 책에서 얕게 다루어서 제대로 알지 못했는데 블로그가 도움이 많이 되었습니다.
    답글

  • Favicon of https://developer87.tistory.com BlogIcon 예은파파 2021.09.03 15:08 신고

    좋은글 잘 보았습니다!

    포스팅해주신 글 중간에 "Config class 에서는 Configuration 속성들을 넣어주면 되는데, 다음에 있을 Global Filter에서 자세하게 이야기해보려 한다.
    현재는 그냥 비어있는 클래스로 넣어주자."

    Global Filter에 관한 글도 혹시 올려주실 예정인가요?ㅎ 찾아봤는데 안보여서 여쭤봅니다 ㅎ
    감사합니다~
    답글