Framework/Spring

[Spring Security] Filter와 Filter Chain에 대한 이해

검은 까마귀 2024. 5. 11. 01:25

2024.05.08 - [Framework/Spring] - [Spring Security] Deep Dive (1)

 

[Spring Security] Deep Dive (1)

# OverviewSpring Security를 쓰면서 뭔가 [어 되네?]에서 그친 이론과 알고리즘을 제대로 이해하고 내가 원하는 User Level 및 다양한 기능을 활용하기 위해서 Deep Dive 해보고자 한다. 이론부터 실전까지! 

blaj2938.tistory.com

 

이전에 Spring Security의 기능 및 역할에 대해서 살펴보았다. 인증, 인가, 보통 수준의 공격(csrf와 같은)으로 부터 방어를 해준다.

 

What을 알았으니 Where를 알아보고자한다.

# 어디서 동작해?

2023.10.26 - [Framework/Spring] - [Spring] Spring MVC 패턴의 LifeCycle

 

[Spring] Spring MVC 패턴의 LifeCycle

# 목적 이번의 면접 질문중에 Spring MVC 패턴의 Process Flow에 대한 인터뷰 질문을 받았습니다. 워낙 급하게 주먹구구식으로 공부하는 편이라 이론?이라고 해야하나? 기본을 잘 모르기 때문에 면접에

blaj2938.tistory.com

 

위의 사진은 Filter가 없는 Spring MVC 패턴이다. Filter는 Dispatcher Servlet앞 단에 위치한다

# Filter란?

Filter의 위치를 확인했다. 그렇다면 Filter는 어떤 일을 하는 걸까?

위치를 보면 알겠지만 DispatherServlet에 요청이 전달되기 전/후에 url 패턴에 맞는 모든 요청에 대해 부가적인 작업을 처리한다.

Filter의 특징 중 하나는 Spring컨테이너 외부에서 동작하기 때문에 스프링 예외처리가 되지 않는다. 

 

일반적으로 필터를 사용할때는 아래와 같다.

  • 요청/응답 로깅: 요청 및 응답 내용을 기록하거나 모니터링 하는 용도로 사용
  • 인증 및 권한 부여: 요청에 대한 인증 및 권한 부여 작업을 수행
  • 데이터 변환: 요청 데이터나 응답 데이터를 변환하거나 형식을 조작(인코딩, 디코딩)
  • 캐싱: 응답을 캐시하여 성능을 향상
  • 예외처리: 예외 상황에 대한 처리를 수

Filter를 여러개 연결하면 Filter Chain으로 활용해서 만들 수 있다. Filter를 사용하는 이유는 중복되는 코드를 최소화할 수 있는데 도움이 된다.

https://docs.spring.io/spring-security/reference/5.8/servlet/architecture.html

 

# 실습

# 실습목표
Filter로 날씨 API를 호출하고 Response에서 "Seoul" ➡️"서울"로 바꿔서 Response받기

# 사용자원

openweather API

Spring Filter

 

 

1. Filter Configuration

import com.black9769.playground.global.config.filter.CustomRequestFilter;
import com.black9769.playground.global.config.filter.CustomResponseFilter;
import com.black9769.playground.global.config.filter.WeatherResponseFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// FilterRegistrationBean을 사용하여 필터를 등록한다.
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<WeatherResponseFilter> weatherResponseFilter(){
        FilterRegistrationBean<WeatherResponseFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new WeatherResponseFilter());
        registrationBean.addUrlPatterns("/api/weather/*");
        //registrationBean.setOrder(2); //Filter Chain의 순서
        return registrationBean;
    }
}

 

먼저, Filter를 Config를 하며, 내가 Filter를 걸고 싶은 Url을 입력한다.

 

2. Create Filter

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

@Slf4j
public class WeatherResponseFilter implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 필요한 경우 초기화 코드 작성
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // HttpServletResponseWrapper를 사용하여 응답 데이터를 가로챔
        ModifyResponseWrapper wrappedResponse = new ModifyResponseWrapper((HttpServletResponse) response);
        chain.doFilter(request, wrappedResponse);

        // 가로챈 응답 데이터를 수정하여 클라이언트로 전송
        byte[] modifiedResponseData = modifyResponseData(wrappedResponse.getData());
        response.getOutputStream().write(modifiedResponseData);
    }

    @Override
    public void destroy() {
        // 필요한 경우 정리 코드 작성
    }

    private byte[] modifyResponseData(byte[] responseData) {
        // HTTP 응답 데이터를 문자열로 변환
        String responseContent = new String(responseData);

        // "Seoul"을 "서울"로 변경
        String modifiedResponseContent = responseContent.replace("\"name\":\"Seoul\"", "\"name\":\"서울\"");

        // 변경된 문자열을 다시 바이트 배열로 변환하여 반환
        return modifiedResponseContent.getBytes();
    }
}

 

Filter를 생성하는 것은 이렇다. 먼저 init을 통해 Filter를 초기화한다. Sl4j를 통해서 제대로 Filter가 생성되었는지 확인 할 수도 있다.

doFilter(): Filter가 되는 주요 로직이다. Wrapper를 통해 응답을 가로채고 값을 수정한다.

destory(): Filter를 지운다.

 

3. Wrapper사용

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class ModifyResponseWrapper extends HttpServletResponseWrapper {
    private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

    public ModifyResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new ServletOutputStream() {
            @Override
            public void write(int b) throws IOException {
                buffer.write(b);
            }

            @Override
            public void flush() throws IOException {
                buffer.flush();
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {

            }
        };
    }

    public byte[] getData() {
        return buffer.toByteArray();
    }
}

 

Response를 가로 채고 수정하기 위한 Modify Wrapper를 생성한다.

HttpServletResponseWrapper

 

HttpServletResponseWrapper 가 기존의 응답을 감싸고 갖고 있다. 그래서 HttpServletResponseWrapper열어서 읽어드린 이후에 modifyResponseData에 넣어서 값을 수정하는 것이다. 단순히 이해하면 쉽다. Response를 가로채기 위한 용도이다.

 

자 이렇게되면 Filter를 동작할 준비는 끝난다. 


이제 실습을 진행하면 된다. 나는 openAPI를 활용했다.

https://openweathermap.org/

 

Current weather and forecast - OpenWeatherMap

Access current weather data for any location on Earth including over 200,000 cities! The data is frequently updated based on the global and local weather models, satellites, radars and a vast network of weather stations. how to obtain APIs (subscriptions w

openweathermap.org

 

단순하게 나의 목표는 내가 날린 요청에 대한 응답중 "Seoul"을 "서울"로 받고 싶었다.

 

Spring에서 먼저 외부API를 호출하기 위해서는 RestTemplate를 사용해야한다.

 

사용하기 위해서 먼저 Bean을 생성해주고

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

 

이렇게 Service단에다가 호출API를 만들어준다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class WeatherService {

    @Value("${openweather.apikey}")
    private String apiKey;

    private final RestTemplate restTemplate;

    public WeatherService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public String getCurrentWeather(String location) {
        String url = "https://api.openweathermap.org/data/2.5/weather?q=" + location + "&appid=" + apiKey;
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
        return response.getBody();
    }
}

 

그리고 컨트롤러에서 호출을하면된다.

 

아래는 결과 값이다.

GET http://localhost:1789/api/weather/current

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 524
Date: Fri, 10 May 2024 16:21:53 GMT

coord":{
    "lon":126.9778,
    "lat":37.5683
},
"weather":[
{
    "id":804,
    "main":"Clouds",
    "description":"overcast clouds",
    "icon":"04n"
}
],
"base":"stations",
"main":{
    "temp":284.81,
    "feels_like":283.49,
    "temp_min":284.81,
    "temp_max":287.84,
    "pressure":1017,
    "humidity":56,
    "sea_level":1017,
    "grnd_level":1011
},
"visibility":10000,
"wind":{
    "speed":3.52,
    "deg":201,
    "gust":13.22
},
"clouds":{
    "all":94
},
"dt":1715357447,
"sys":{
    "type":1,
    "id":5509,
    "country":"KR",
    "sunrise":1715372784,
    "sunset":1715423437
},
"timezone":32400,
"id":1835848,
"name":"서울",
"cod":200
}
Response code: 200; Time: 237ms (237 ms); Content length: 520 bytes (520 B)

Cookies are preserved between requests:
> C:\Users\black\IdeaProjects\Spring-Playground\.idea\httpRequests\http-client.cookies

 

name에 서울로 바뀐것을 볼 수 있다.

반응형

'Framework > Spring' 카테고리의 다른 글

[Spring Security] Intro. Deep Dive  (0) 2024.05.08
[Spring] DB 접근 방법  (0) 2024.04.10
[Spring] Open API 활용하기  (0) 2023.12.15
[Spring] Rest Doc 활용하기  (0) 2023.12.13
[Spring] Spring MVC 패턴의 LifeCycle  (0) 2023.10.26