ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 8. [MSA 구현 퀵스타트] API G/W Zuul 초간단 구현 2
    Spring Cloud 2021. 7. 3. 18:17

    지난 장에서는 Zuul의 필터에서 단순한 로그를 출력함으로써 Pre / Route / Post 필터의 동작을 확인하였습니다.

    이번 장에서는 지난장에 이어서 Zuul API Gateway의 실무에서 거의 필수적으로 구현되는 몇 가지 기능을 좀 더 확장하여 구현해 보겠습니다.

     

     

    이번장 구현할 내용

    1. 요청 URI와 Http Body를 추출하여 로깅합니다.
    2. 인증 : 유효한 요청인지 확인하기 위하여, Http Header 담긴 Authorzation을 추출하여 인증합니다.
      * 정상 요청이 아닐 경우 Http Status 401(Unauthorized)을 반환하고, 서비스로 라우팅을 하지 않습니다.
    3. Http Header에 추가적인 커스텀 정보 담아서 서비스에 요청 보냅니다.
    4. 서비스로 부터 받은 응답 http Status와 Http Body를 추출하여 로깅합니다.

    요청 로깅, 인증, Http Header 추가는 전처리 작업이기에 PreFilter에서 응답 로깅은 후처리 작업임으로 PostFilter에서 각각 처리합니다.

     

    예제 소스

    이번장에 진행될 소스는 아래 Github에서 다운로드하시고,"3.Api-Gateway2" branch를 체크아웃해주세요.

     

    프로젝트의 기본 구조와 파일은 전 장과 동일합니다.

    소스 구조

     

    요청 로깅과 인증 구현

    우선 요청을 로깅하고 인증하는 PreFilter의 전체 소스를 확인해 보겠습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
        @Override
        public Object run() {
            RequestContext ctx = RequestContext.getCurrentContext();
            log.info("===== START Pre Filter. =====");
     
            String reqUri = ctx.getRequest().getRequestURI();
            String reqHttpBody = getReqHttpBody(ctx);
            log.info("[Pre Filter] : Request reqUri : {} HttpBody : {}", reqUri, reqHttpBody);
     
            String authorization = ctx.getRequest().getHeader(HttpHeaders.AUTHORIZATION);
            if(StringUtils.isEmpty(authorization) || !AUTHORIZATION_VALUE.equals(authorization)) {
                respError(ctx);
            }
     
            ctx.addZuulRequestHeader("foo""bar");
            return null;
     
        }
     
        private void respError(RequestContext ctx) {
            try {
                ctx.setSendZuulResponse(false);
                ctx.getResponse().sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
     
        private String getReqHttpBody(RequestContext ctx) {
            String reqHttpBody = null;
            try {
                InputStream in = (InputStream) ctx.get("requestEntity");
                if (in == null) {
                    in = ctx.getRequest().getInputStream();
                    reqHttpBody = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
                }
            } catch (IOException e) {
                log.error("It is failed to obtainning Request Http Body.", e);
                return "";
            }
            return reqHttpBody;
        }
    cs

    RequestContext 객체는 Http Servlet의 요청과 응답, 상태 및 기타 Zuul의 다양한 정보들을 담고 있습니다.

    RequestContext 객체로부터 HttpServletRequest를 가져와 로깅에 필요한 Http Body요청 Uri, 그리고 인증에 필요한 Http Header 정보를 추출할 수 있습니다.

     

    로깅 구현

    요청 URI는 String reqUri = ctx.getRequest(). getRequestURI(); 로 간단히 가져올 수 있습니다. 

     

    요청 Http Body 정보는 getReqHttpBody(RequestContext ctx)  메서드로 구현하였습니다.

    RequestContext 객체에서 InputStream으로 추출하여 StreamUtils 유틸로 String으로 변환하여 반환합니다.

     

    인증 구현

    API 요청 시에 인증을 위해서 Http Header의 Authorzation에 유효한 키값을 담아서 전송하게 됩니다.

    Zuul에서는 해당 정보를 추출하여 키값이 유효한 경우에 API 라우팅을 하며, 키 값이 없거나 유효하지 않은 경우에는 401 Unauthorized 에러를 반환하도록 구현하였습니다.

     

    원칙적으로는 Authorzation의 키값은 좀 더 복잡한 과정으로 만들어져 요청되고, 검증 또한 같은 방법으로 해독하여 검증하여야 하나, 현재 포스팅은 암호화 인증 관련 내용이 아니므로 간단히 Authorzation의 키값을 "12345"로 하드 코딩하였습니다.

     

    즉, 헤더로 부터 추출한 Authorzation의 값이 "12345"인지 단순 비교하여 맞으면 통과, 틀리면 에러를 반환하도록 단순 구현하였습니다.

    1
    2
    3
    4
            String authorization = ctx.getRequest().getHeader(HttpHeaders.AUTHORIZATION);
            if(StringUtils.isEmpty(authorization) || !AUTHORIZATION_VALUE.equals(authorization)) {
                respError(ctx);
            }
    cs

     

    커스텀 헤더의 추가는 RequestContext의 addZuulRequestHeader 메서드에 추가할 Key:Value를 담으면 됩니다.

     

    ctx.addZuulRequestHeader("foo""bar");

     

     

    서비스 1에서 커스텀 Http Header 확인

    서비스 1에서는 Zuul에서 추가된 커스텀 Http Header(foo)의 값을 꺼내어 확인해 보겠습니다.

    1
    2
    3
    4
    5
        @GetMapping(value = "/ribbon/{id}")
        public String ribbon(@PathVariable("id"String id, @RequestHeader("foo"String foo) {
            log.info("Http Header foo : {}", foo);
            return discoveryService.ribbon(id);
        }
    cs

    서비스 1의 "/ribbon/{id}" API를 호출할 경우 추가된 header값을 확인할 수 있도록 추가 구현하였습니다.

    • @RequestHeader 은 Controller에서 요청으로 전달된 특정 Http Header를 가져올 수 있는 애너테이션 입니다.

     

    응답 로깅 구현

    이번에는 응답 로깅하는 전체 소스를 확인해 보겠습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
        @Override
        public Object run() {
            RequestContext ctx = RequestContext.getCurrentContext();
            log.info("===== START Post Filter. =====");
     
            int httpStatus = ctx.getResponse().getStatus();
            String respHttpBody = getRespHttpBody(ctx);
            log.info("[Post Filter] HttpStatus : {}, HttpBody : {}", httpStatus, respHttpBody);
     
            return null;
        }
     
     
        private String getRespHttpBody(RequestContext ctx) {
            String responseBody = ctx.getResponseBody();
            if (responseBody == null) {
                InputStream is  = ctx.getResponseDataStream();
                try {
                    byte[] ib = StreamUtils.copyToByteArray(is);
                    responseBody = new String(ib, StandardCharsets.UTF_8);
                    ctx.setResponseDataStream(new ByteArrayInputStream(ib));
                } catch (IOException e) {
                    log.error("It is failed to obtainning Response Http Body.", e);
                }
            }
            return responseBody;
        }
    cs

     

    추출하는 내용은 String getRespHttpBody(RequestContext ctx)로 구현하였습니다. 내용을 보면 이해하시는데 크게 어렵지 않으시리라 생각됩니다.

    주의하실 사항은 RequestContext getResponseDataStream() 메서드로 응답 Body를 추출하면, 다시 setResponseDataStream() 메서드에 InputStream으로 응답 원본을 담아주어야 합니다.

    그렇지 않고  getResponseDataStream() 메서드만 호출할 경우 최종 클라이언트로 응답 Body가 없이 반환되게 됩니다.

     

    • setResponseDataStream() 메서드를 응용하면 응답은 변환하는 작업도 가능해집니다.

     

     

    동작 확인

    구현이 완료되었으니 실행을 해보도록 하겠습니다.

    앞서 구현한 유레카 서버, 서비스 1, 서비스 2와 API Gateway를 모드 실행합니다.

     

    첨부된 Postman의 "API G/W - name (by Ribbon)" 요청에 아래의 사항을 살짝 추가합니다.

    1. Http Body 추가
    2. Authorzation

    현재 구현된 서비스 예제에는 요청 Body가 필요 없기에 설정하지 않았습니다.

    서비스의 동작에 의미는 없지만 로깅을 위해서, Body에 JSON이나 String 등 아무 값이나 담아봅니다.

    저는 {"AA":"11""BB":"22"}와 같은 JSON을 담아서 호출 하였습니다.

     

    마찬가지로 Headers에 인증키로 "12345"를 추가해 봅니다.

     

    이제 설정이 완료되었으니 Postman을 호출한 뒤 로그를 확인해 봅시다. 아래와 같이 로깅된 정보를 확인하실 수 있으십니다.

    Zuul의 로깅 정보
    서비스1의 커스텀 Http Header 로깅

     

    이번에는 잘못된 인증키로 요청한 뒤 오류 응답을 확인해 봅니다.

    Postman의 Headers에 인증키로 "12345" 이외의 아무 값으로 변경한 뒤 호출해 봅니다.

    아래와 같이 Http Status 401 Unauthorized 오류 응답이 반환되고, 해당 API는 실행되지 않은 것을 알 수 있습니다.

    Zuul은 예외 발생 혹은 오류를 반환하더라도 Post 필터가 실행이 됩니다. 오류 응답 이후에서 Post Log가 출력됨을 확인 할 수 있습니다. 단, 서비스1로 라우팅 되지 않았기에 서비스1로 부터 응답 받은 Http Status와 Http Body가 없기에

    아래와 같이 출력됨을 알 수 있습니다. 

    라우팅 없이 오류 응답시 Post 필터 실행

     

     

    마무리

    이번장에서는 API Gateway의 여러 기능 중에서 실무에서 가장 많이 쓰고 기본적인 사항을 구현해 보았습니다.

     

    추가로 아래의 사항을 좀 더 보완한다면 더 완벽한 Zuul을 구현하실 수 있을 겁니다.

    1. 오류 시에 Zuul이 응답하는 Http Status와 Body를 로깅 하기
    2. 오류 시에 스프링이 기본적으로 반환하는 응답이 아닌 커스텀 응답 만들어 반환하기

    Zuul의 내용이 너무 길어지기 여기서 마무리 하고, 추후 기회가 된다면 Zuul에 대해 추가 포스팅을 해보겠습니다.

     

    다음 장에서는 장애 대응 라이브러리인 Hytrix를 구현해 보겠습니다.

     

     

    참고문헌

    - 스프링 마이크로 서비스 코딩 공작소 정성권 옮김 길벗

    - https://github.com/spring-cloud-samples/sample-zuul-filters

    댓글

Designed by Tistory.