Spring Cloud Gateway 실습

광부·2024년 5월 28일
1

Spring

목록 보기
6/8

스프링에는 Reactive 라이브러리와 mvc 라이브러리가 존재하고

이들의 차이점은 비동기 지원 여부에 따라 나뉨.

Reactive Gateway 의존성을 추가하면 Reactive web 의존성이 추가로 설치됨

Gateway(mvc) 의존성을 추가하면 web 의존성이 추가로 설치됨.

둘의 차이점은 동기 방식(tomcat)과 비동기 방식(netty)의 처리.

Eureka Client 설정

다른 서비스들과 마찬가지로 Eureka 서버에 등록하기 위해 Eureka Client 의존성을 추가하여 설정해준다.

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka

Routes 설정

spring:
  cloud:
    gateway:
      routes:
        - id: user-service # 로그인
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user-service/login
            - Method=POST
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
        - id: user-service # 회원가입
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user-service/users
            - Method=POST
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
        - id: user-service #
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user-service/**
            - Method=GET
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
            - AuthorizationFilter
        - id: catalog-service # 카탈로그 서비스
          uri: lb://CATALOG-SERVICE
          predicates:
            - Path=/catalog-service/**
          filters:
            - RewritePath=/catalog-service/(?<segment>.*), /$\{segment}
        - id: order-service # 주문 서비스
          uri: lb://ORDER-SERVICE
          predicates:
            - Path=/order-service/**
          filters:
            - RewritePath=/order-service/(?<segment>.*), /$\{segment}
  • RewritePath 필터 사용 이유 : 게이트웨이에서 predicates의 Path가 일치하여 routing될 때, lb://{service-nam}/{predicates.Path} 로 라우팅이 된다.
    • 즉, 사용자가 {게이트웨이}/user-service/login 에 요청을 보낸다면 게이트웨이는 lb:/USER-SERVICE/user-service/login으로 요청을 보낸다.
    • 그렇다면 USER-SERVICE에 정의한 엔드포인트에서는 모두 user-service라는 접두사를 붙여야하는데, 이를 대체할 수 있는 방법이 RewritePath 필터이다.
    • 이는 라우팅할 경로를 재정의할 수 있기에 user-service/login을 통째로 전달하는 것이 아닌 user-service를 제외한 /login 의 경로만 전달 가능하다.

AuthorizationFilter - 커스텀 필터

@Component
@Slf4j
public class AuthorizationFilter extends AbstractGatewayFilterFactory<AuthorizationFilter.Config> {
    Environment env;
    public AuthorizationFilter(Environment env){
        super(Config.class);
        this.env=env;
    }

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

            if(!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)){
                return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
            }

            String authorizationHeader = request.getHeaders().get(org.springframework.http.HttpHeaders.AUTHORIZATION).get(0);
            String jwt = authorizationHeader.replace("Bearer ","");
            if(!isJwtValid(jwt)){
                return onError(exchange, "Jwt token is not valid", HttpStatus.UNAUTHORIZED);
            }

            return chain.filter(exchange);
        };
    }

    private boolean isJwtValid(String jwt) {
        boolean returnValue = true;
        byte[] secretKeyBytes = Base64.getEncoder().encode(env.getProperty("token.secret").getBytes());
        SecretKey secretKey = Keys.hmacShaKeyFor(secretKeyBytes);
        String subject=null;

        JwtParser parser = Jwts.parser().verifyWith(secretKey).build();
        try{
            subject = parser.parseSignedClaims(jwt).getPayload().getSubject();
        }catch (Exception ex){
            returnValue = false;
        }

        if(subject==null||subject.isEmpty()){
            returnValue = false;
        }

        return returnValue;
    }
    // Mono,Flux 란? Webflux에서 데이터를 처리하는 단위 단일 이냐 다중이냐에 따라서 구분해서 사용됨
    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response= exchange.getResponse();
        response.setStatusCode(httpStatus);

        log.error(err);
        return response.setComplete();
    }

    public static class Config{
    }
}
  • exchange.getRequest 에서 현재 요청을 가져와서
  • Authorization 헤더가 있는지 확인 후, 없다면 Unauthorized 401 상태코드를 반환한다.
  • 있다면 Bearer 를 제거한 후, isJwtValid 메서드를 실행한다.
  • 환경변수로 설정한 secret값을 사용하여 subject를 확인하고, subject가 없거나 null값이 들어있다면 Unauthorized 401 상태코드를 반환한다.

여기서 AbstractGatewayFilterFactory<C> 를 상속받아 사용자 정의 필터를 만들었다. 제너릭타입으로는 커스터 필터인 AuthorizationFilter 내부에 정적 클래스 Config 를 할당해주었다.

제너릭 타입으로 들어가는 Config클래스는 필터의 설정을 추가하기 위한 용도로 사용되지만, 설정 없이도 구현 가능하여 클래스 정의만 해두었다.

AuthorizationFilter@Component 어노테이션을 사용해 스프링 빈으로 등록해두었다.

따라서 위의 Yaml 설정에서 AuthorizationFilter 이름만으로 원하는 로직에 필터를 적용시킬 수 있다.

github 참고)
https://github.com/ajou20658/spring-cloud-toy/tree/master/gateway

profile
백엔드 주니어 개발자

0개의 댓글