스프링 부트 애플리케이션에서 interceptor와 filter를 활용하여 요청 매개변수의 유효성을 검증하는 방법을 알아본다.
1. HttpServletRequestWrapper 재정의하여 본문 읽기
요청 본문(request body)을 읽기 위해 HttpServletRequestWrapper를 상속하는 클래스를 만든다. 파일 업로드 케이스도 함께 처리한다.
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class RepeatedRequestWrapper extends HttpServletRequestWrapper {
private final String requestBody;
public RepeatedRequestWrapper(HttpServletRequest httpRequest) {
super(httpRequest);
String contentType = httpRequest.getContentType();
String multiPartMethod = "multipart/form-data";
if (contentType != null && contentType.contains(multiPartMethod)) {
httpRequest = new StandardServletMultipartResolver().resolveMultipart(httpRequest);
}
StringBuilder payloadBuilder = new StringBuilder();
BufferedReader reader = null;
InputStream inputStream = null;
try {
inputStream = httpRequest.getInputStream();
if (inputStream != null) {
reader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = reader.read(charBuffer)) > 0) {
payloadBuilder.append(charBuffer, 0, bytesRead);
}
} else {
payloadBuilder.append("");
}
} catch (IOException exception) {
// 예외 처리
} finally {
closeQuietly(inputStream);
closeQuietly(reader);
}
requestBody = payloadBuilder.toString();
}
private void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
// 로그 기록
}
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteStream = new ByteArrayInputStream(requestBody.getBytes());
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return byteStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
public String getRequestBody() {
return this.requestBody;
}
}
2. 필터 생성하여 요청 래핑
요청 본문은 한 번만 읽을 수 있으므로, 래퍼된 요청을 필터 체인으로 전달한다.
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component
@WebFilter(urlPatterns = "/*", filterName = "requestBodyFilter")
public class RequestBodyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
ServletRequest wrappedRequest = null;
if (servletRequest instanceof HttpServletRequest) {
wrappedRequest = new RepeatedRequestWrapper((HttpServletRequest) servletRequest);
}
if (wrappedRequest == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(wrappedRequest, servletResponse);
}
}
@Override
public void destroy() {
}
}
3. Interceptor로 본문 검증하기
POST 요청에 대해 매개변수 유효성 검사를 수행한다.
@Component
public class RequestParamInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String httpMethod = request.getMethod();
String contentType = request.getContentType() == null ? "" : request.getContentType();
if (HttpMethod.POST.name().equals(httpMethod)
&& !contentType.equals(MediaType.MULTIPART_FORM_DATA_VALUE)) {
RepeatedRequestWrapper wrappedRequest = new RepeatedRequestWrapper(request);
String body = wrappedRequest.getRequestBody();
validateRequestParams(body);
}
return true;
}
private void validateRequestParams(String param) {
try {
String sanitized = param.replace(" ", "")
.replace("&", "")
.replace("#", "")
.replace("\"\"", "");
if (sanitized.toLowerCase().contains("<script")
|| sanitized.contains("<")
|| sanitized.contains(">")
|| sanitized.toLowerCase().contains("<html")
|| sanitized.toLowerCase().contains("<header")
|| sanitized.toLowerCase().contains("alert")
|| sanitized.toLowerCase().contains("console")) {
throw new CustomException("INVALID_PARAMETER", "잘못된 매개변수입니다!");
}
} catch (Exception ex) {
if (ex instanceof CustomException) {
throw new CustomException("INVALID_PARAMETER", ((CustomException) ex).getMessage());
}
}
}
}
4. Interceptor 설정
WebMvcConfigurer를 통해 interceptor를 등록한다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private RequestParamInterceptor paramInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(paramInterceptor)
.addPathPatterns("/**");
}
}
5. 필터 활성화
애플리케이션 시작 클래스에 @ServletComponentScan 어노테이션을 추가한다.
@SpringBootApplication
@ServletComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
6. 민감한 단어 필터링
외부 라이브러리를 사용하여 민감한 단어를 필터링할 수 있다. 직접 검증 로직을 작성하지 않아도 되어 편리하다.
Maven 의존성 추가:
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-word</artifactId>
<version>0.2.0</version>
</dependency>
Interceptor에 민감한 단어 검증 로직 추가:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String httpMethod = request.getMethod();
String contentType = request.getContentType() == null ? "" : request.getContentType();
if ((HttpMethod.POST.name().equals(httpMethod) || HttpMethod.PUT.name().equals(httpMethod))
&& !contentType.equals(MediaType.MULTIPART_FORM_DATA_VALUE)) {
RepeatedRequestWrapper wrappedRequest = new RepeatedRequestWrapper(request);
String body = wrappedRequest.getRequestBody();
if (SensitiveWordHelper.contains(body)) {
List<String> sensitiveWords = SensitiveWordHelper.findAll(body, WordResultHandlers.word());
throw new CustomException("SENSITIVE_WORD",
MessageFormat.format("민감한 단어가 포함되어 있습니다: {0}", sensitiveWords));
}
}
return true;
}
위와 같은 방식으로 스프링 부트 애플리케이션의 요청 매개변수를统一적으로 검증할 수 있다.