
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를 사용하는 이유는 중복되는 코드를 최소화할 수 있는데 도움이 된다.

# 실습
# 실습목표
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를 활용했다.
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 |