2024.05.08 - [Framework/Spring] - [Spring Security] Deep Dive (1)
이전에 Spring Security의 기능 및 역할에 대해서 살펴보았다. 인증, 인가, 보통 수준의 공격(csrf와 같은)으로 부터 방어를 해준다.
What을 알았으니 Where를 알아보고자한다.
# 어디서 동작해?
2023.10.26 - [Framework/Spring] - [Spring] Spring MVC 패턴의 LifeCycle
위의 사진은 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를 활용했다.
단순하게 나의 목표는 내가 날린 요청에 대한 응답중 "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 |