Servlet 에 대해 - Filter & Wrapper

제훈·2024년 8월 7일

Java

목록 보기
30/34

Filter : HTTP 요청과 응답 사이에서 전달되는 데이터를 가로채어, 서비스에 맞게 변경하고 걸러내는 필터링 작업을 수행한다.

  • 필터 설정에 따라 해당하는 요청 및 응답 시에 반드시 거쳐야 하며, 비밀번호 암호화 처리, 인코딩 설정 등 공통 관리에 해당하는 기능을 수행할 수 있다.

  • 필터는 인증 필터, 압축 필터, 리소스 접근 트리거 이벤트 필터, 로깅 필터, 이미지 변환 필터, 토크나이져 필터 등 다양하게 활용 가능하다.

Servlet Filter 처리 내용

  • Request에 대한 처리
    • 보안 관련 사항
    • 요청 header와 body 형식 지정
    • 요청에 대한 log 기록 유지
  • Response에 대한 처리
    • 응답 stream 압축
    • 응답 stream 내용 추가 및 수정
    • 새로운 응답 작성
    • 여러 가지 필터를 연결(= chain, 서로 호출)하여 사용할 수 있다.

동작 구조

세션은 서블릿의 실행 전, 후로 동작하기 때문에 서블릿의 service() 메소드 실행 전, 후에 작동한다.

  • 필터가 여러 개라면 stack 방식으로 순차적으로 수행된다.

사진으로 보자.

Filter Chain (Interface)

위에서 말한대로 Filter가 여러 개인 경우에는 Filter Chain으로 작동할 수 있다.

  • Chain처럼 서로 연결되어 있는 FilterdoFilter() 메소드를 이용하여 순차적으로 실행시키는 인터페이스이다.
  • doFilter() 메소드는 chain으로 연결되어 있는 다음 필터 또는 서블릿을 실행하는 메소드이다.

한 번 활용 예제를 보자.


활용 예제

index.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1 align="center">Filter</h1>
<h3>필터의 라이프 사이클</h3>
<ul>
    <li><a href="first/filter">Filter 사용하기</a></li>
</ul>
</body>
</html>
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

@WebFilter("/first/*")
public class FirstFilter implements Filter {

    public FirstFilter() {
        System.out.println("FirstFilter 인스턴스 생성");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("FirstFilter init 호출됨");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("FirstFilter doFilter 호출됨");

        /* 설명. filterChain에서 제공하는 doFilter를 활용하여 다음 필터 또는 서블릿을 진행시킬 수 있다. */
        filterChain.doFilter(servletRequest, servletResponse);
        // 위의 doFilter로부터 서블릿 -> 서비스 -> 리포지토리 -> DB 를 다녀온다.

        System.out.println("서블릿을 다녀온 후");

    }

    @Override
    public void destroy() {
        System.out.println("FirstFilter destroy 호출됨");
    }
}

위의 코드에서 Filter는 아래 코드로 이동해서 doGet() 메소드가 호출된다.

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet("/first/filter")
public class FirstFilterTestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("서블릿으로 get 요청 확인");
    }
}

실행 시 화면


활용 예제 2

위의 예제에 추가를 해보자.

일단 하기 전에 build.gradle에 추가할게 있다.
dependency에 추가하자.

    // https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto
    implementation 'org.springframework.security:spring-security-crypto:5.7.3'
    // https://mvnrepository.com/artifact/commons-logging/commons-logging
    implementation 'commons-logging:commons-logging:1.2'

index.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1 align="center">Filter</h1>
<h3>필터의 라이프 사이클</h3>
<ul>
    <li><a href="first/filter">Filter 사용하기</a></li>
</ul>

<hr>
<!--build.gradle에 단방향 암호화를 위한 bcrypt 관련 라이브러리 추가ㅣ
(2개의 라이브러리 추가)-->
<h3>필터의 활용</h3>
<form action="member/regist" method="post">
    <label for="test">아이디: </label>
    <input id="test" type="text" name="userId">
    <br>
    <label>비밀번호: </label>
    <input type="password" name="password">
    <br>
    <label>이름: </label>
    <input type="text" name="name">
    <br>
    <button type="submit">가입하기</button>
</form>

</body>
</html>

RegisterMemberServlet

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import java.io.IOException;

@WebServlet("/member/register")
public class RegisterMemberServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String userId = req.getParameter("userId");
        String password = req.getParameter("password");
        String name = req.getParameter("name");

        System.out.println("userId = " + userId);
        System.out.println("password = " + password);
        System.out.println("name = " + name);

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        System.out.println("비밀번호가 pass01인지 확인 : " + passwordEncoder.matches("pass01", password));
        System.out.println("비밀번호가 pass02인지 확인 : " + passwordEncoder.matches("pass02", password));
    }
}

PasswordEncryptFilter

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

@WebFilter("/member/*")
public class PasswordEncryptFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("패스워드 암호화 필터의 doFilter 실행");

        RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);

        filterChain.doFilter(requestWrapper, servletResponse);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

RequestWrapper

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class RequestWrapper extends HttpServletRequestWrapper {
    public RequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String key) {

        /* 설명. 'password'라는 키 값이 들어오면 암호화를 하는 우리만의 getParameter 메소드 재정의 */
        String value = "";
        if ("password".equals(key)) {
            System.out.println("패스워드 꺼낼 시");
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            value = passwordEncoder.encode(super.getParameter("password"));
        } else {
            value = super.getParameter(key);
        }

        return value;
    }
}

EncodingFilter

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

@WebFilter("/member/*")
public class EncodingFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        /* 설명. 우린 톰캣 10 버전인데 톰캣 10 버전 미만일 경우에는 post 요청에 대해 인코딩 설정을 해줘야 한다. */
        /* 설명. 필터를 활용해 request 객체에 인코딩 설정을 적용하고(전처리) 다음 필터나 서블릿으로 넘겨준다. */
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        if("POST".equals(httpServletRequest.getMethod())) {
            httpServletRequest.setCharacterEncoding("UTF-8");
        }

        filterChain.doFilter(httpServletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

실행했을 때의 출력되는 내용들

user01
pass01
홍길동

위 데이터를 넣어봤다.

필터가 여러 개이므로 index.jsp 에서 가장 먼저 부른건 RegisterMemberServlet 클래스이어도, 잘 보면 @WebFilter 어노테이션들로 이루어진 나머지 클래스들이 stack 형태로 실행되는 것이다.

순서는? -> 직접 처리해줄 수도 있고 냅두면 자동으로 한다.

잘 보면 Wrapper에 대해서도 적혀 있는데 자료형에서 나온 Wrapper 클래스와 다른 것이다.


Servlet Wrapper

  • 관련 클래스(ServletRequest, ServletResponse, HttpServletRequest,
    HttpServletResponse)를 내부에 보관하며 해당 인터페이스를 구현한 객체를 참조하여 구현 메소드를 위임한다.

  • Java Event처리의 Adapter Class와 비슷한 기능을 한다고 볼 수 있다.

  • 사용자가 별도의 request나 response 객체를 생성하여 활용할 때 Wrapper Class를 상속하여 활용하면, 편하게 원하는 Class만 재정의하여 사용할 수 있다.

Wrapper Class

  • HttpServletRequestWrapper
    • 요청한 정보를 변경하는 Wrapper Class로, HttpServletRequest 객체를 매개로 하는 생성자를 가진다.
      public SampleWrapper(HttpServletRequest wrapper) {
      	super(wrapper);
      }
  • HttpServletResponseWrapper
    • 응답할 정보를 변경하는 Wrapper Class로, HttpServletResponse 객체를 매개로 하는 생성자를 가진다.
      public SampleWrapper(HttpServletResponse wrapper) {
      	super(wrapper);
      }

참고 : 암호화 및 Bcrypt

예제 2에서 추가한 Bcrypt에 대한 이야기다.

이 그림은 중간에 패킷 정보를 빼내 가져가는 해킹 기법인 패킷스니핑 기법으로 대비하기 위해 암호화처리가 필요하다.

물론 저런 해킹에 대비해서 DB에도 암호화된 데이터가 필요하다.

  • 서버는 양방향 암호화 처리를 한다.
    • 암호화란 평문을 다이제스트(= 기존 문자열을 변환한 일정 길이의 문자열)로 변경하는 것이고, 복호화는 다이제스트를 다시 평문으로 변경하는 것이다.
    • 이때 암호화는 가능하지만 복호화는 불가한 것이 단방향 암호화이고, 암호화와 복호화 모두 가능한 것이 양방향 암호화이다.
  • 저장한 서버도 관리자도 알아서는 안되는 고객의 개인 정보 등이 있을 수 있으므로 데이터베이스는 단방향 암호화 처리를 한다.

💡 BCrypt 암호화
1. 비밀번호를 데이터베이스에 저장할 목적으로 설계된 암호화 알고리즘이다.
2. 랜덤 솔팅 기법을 적용한 다이제스트 생성을 지연시킨 단방향 해시 암호화 알고리즘이다.
3. 암호화는 가능하나 복호화가 불가능한 높은 수준의 암호화 알고리즘이다.

  • 해시 알고리즘이란 어떤 메시지를 넣더라도 해시 함수를 통과하면 동일한 길이의 랜덤한 문자열이 반환되는 것으로, 빠른 속도로 다이제스트 생성이 가능하다. 이러한 해시 알고리즘은 다이제스트 생성 방식이 동일하므로 아래 두 가지 방법으로 패턴만 알아내면 복호화가 가능해진다.
    1. 무차별 대입 → 모든 경우의 수에 대해 다이제스트를 생성해 맞춰보면서 패턴 탐색
    2. 사전식 대입 → 사전 속 단어를 다이제스트로 변경해 다이제스트와 비교해 패턴 탐색

따라서 위와 같은 패턴 탐색을 방지하고자 Bcrypt 암호화에서 적용한 salting 기법은 원문만 암호화하지 않고 지정한 다른 글자(= salt값)를 붙여 암호화하는 것이고, BCrypt는 암호화할 때마다 랜덤한 salt값을 이용한다.

profile
백엔드 개발자 꿈나무

0개의 댓글