공부하기/Spring

[Spring] LoginCheckFilter 구현 : 로그인/아웃 로직 구현하기

다섯자두 2025. 2. 10. 10:49

Filter 알아보기

Filter 실행 흐름

HTTP 요청 → WAS → `Filter 1 → Filter 2 → ... → Filter N` → Servlet → Controller 
  • Servlet이 호출되기 전에 Filter를 항상 거치게 된다.
  • 따라서 Controller에서 수행할 공통 관심 사항을 Filter에 구현하면 모든 요청 혹은 응답에 적용할 수 있다.
  • 특정 URL Pattern에만 Filter를 등록할 수도 있다.

Filter의 구현

Filter Interface

`jakarta.servlet.Filter` 인터페이스를 구현하여야 한다.

  • 주요 메서드는 `init`, `doFilter`, `destroy`로 이루어져 있다.
    1. HTTP 요청이 오면 `doFilter` 메서드가 호출된다.  ← 원하는 로직을 이 메서드에 구현한다.
    2. 필터를 초기화/종료할 때 `init`/`destroy` 메서드가 호출된다.
  • 구현한 Filter를 Filter Chain에 등록하여 사용한다.

Filter의 등록

SpringBoot에서 Filter를 등록하는 방법은 크게 두 가지가 있다.

(1) Servlet 방식

Servlet Container(Tomcat)가 직접 필터를 관리하는 방식이다.

`@WebFilter` 어노테이션으로 필터를 등록한다.

@WebFilter(urlPatterns = "/*")  // 모든 요청에 대해 필터 적용
public class MyServletFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("MyServletFilter 초기화됨");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("MyServletFilter 실행됨");
        chain.doFilter(request, response); // 다음 필터 또는 컨트롤러로 요청 전달
    }

    @Override
    public void destroy() {
        System.out.println("MyServletFilter 종료됨");
    }
}
  1. Spring이 아닌 순수 Servlet 환경에서도 동작이 가능하다.
  2. 필터 순서를 지정할 수 없다. (web.xml을 사용하면 특정 태그를 이용해 순서를 조정할 수 있기는  하지만 번거로우며, Spring Boot에서는 web.xml을 잘 사용하지 않는다.)

(2) Spring 방식

Spring IoC 컨테이너에서 필터를 관리하는 방식으로, `FilterRegistrationBean<T extends Filter>`클래스와 `@Bean` 어노테이션을 사용하여 필터를 등록한다.

 

📌 1) 필터 클래스 구현

@Component
public class MySpringFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("🌱 MySpringFilter 실행됨");
        chain.doFilter(request, response);
    }
}

 

📌 2) FilterRegistrationBean을 사용하여 필터 등록

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<MySpringFilter> loggingFilter() {
        FilterRegistrationBean<MySpringFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new MySpringFilter());  // 필터 등록
        registrationBean.setOrder(1);  // 실행 순서 지정 (낮을수록 먼저 실행됨)
        registrationBean.addUrlPatterns("/*");  // 모든 URL에 대해 필터 적용
        return registrationBean;
    }
}

SpringBoot는 FilterRegistrationBean<>을 @Bean으로 등록하면 자동으로 이를 Spring Filter Chian에 추가한다.


🧐 Servlet Container 필터와 Spring 필터를 동시에 사용할 수 있을까?

yes. 둘을 모두 사용할 수 있다.

[사용자 요청]
   ↓
[Servlet Container (Tomcat)]
   ↓
[Servlet Filter (`@WebFilter`) 실행 (필요한 경우)]
   ↓
[DispatcherServlet (Spring MVC)]
   ↓
[Spring Filter Chain 실행 (`FilterRegistrationBean` 등록 필터 포함)]
   ↓
[Spring Security Filter 실행 (필요한 경우)]
   ↓
[Controller 실행]

로그인/아웃 로직을 구현해보자

AuthController

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    @PostMapping("/login")
    public ResponseEntity<Void> login(
            @Valid @RequestBody LoginUserRequest request,
            HttpServletRequest httpRequest,
            HttpServletResponse httpResponse
    ) {
        authService.login(request, httpRequest, httpResponse);
        return new ResponseEntity<>(HttpStatus.OK);
    }

    @PostMapping("/logout")
    public ResponseEntity<Void> logout(
            HttpServletRequest httpRequest,
            HttpServletResponse httpResponse
    ) {
        authService.logout(httpRequest, httpResponse);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

로그인

  • http body로 LoginUserRequest를 받아오고 HttpServletRequest, HttpServletResponse를 service에 함께 전달한다.

로그아웃

  • HttpServletRequest, HttpServletResponse를 service에 전달한다.

AuthService

/**
 * 로그인, 인증, 세션/쿠키 관리
 */
@Service
@RequiredArgsConstructor
public class AuthService {
    private final UserService userService;

    private final PasswordEncoder passwordEncoder;

    public void login(
            LoginUserRequest request,
            HttpServletRequest httpRequest,
            HttpServletResponse httpResponse
                      ) {
        User user = authenticate(request.getEmail(),request.getPassword());

        // 세션 생성
        HttpSession session = httpRequest.getSession(true);
        session.setAttribute("user",user.getId());

        // 세션 ID 쿠키로 전달 -> Spring Boot가 자동으로 추가해줌
//        Cookie sessionCookie = new Cookie("JSESSIONID", session.getId());
//        sessionCookie.setHttpOnly(true);
//        sessionCookie.setPath("/");
//        sessionCookie.setMaxAge(60 * 60);
//        httpResponse.addCookie(sessionCookie);
    }

    private User authenticate(String email, String password) {
        User user = userService.getUserByEmail(email);
        checkPasswordMatch(password,user.getPassword());
        return user;
    }

    private void checkPasswordMatch(String inputPassword, String storedPassword) {
        if(!passwordEncoder.matches(inputPassword, storedPassword)) {
            throw new CustomException(ExceptionType.INVALID_CREDENTIALS);
        }
    }

    public void logout(
            HttpServletRequest httpRequest,
            HttpServletResponse httpResponse
    ) {
        HttpSession session = httpRequest.getSession(false);
        if(session != null) {
            session.invalidate();
        }

        // delete sessionId cookie
        Cookie sessionCookie = new Cookie("JSESSIONID", null);
        sessionCookie.setMaxAge(0);
        sessionCookie.setPath("/");
        httpResponse.addCookie(sessionCookie);
    }
}

로그인

  • ``Line 16`` : LoginUserRequest로 이메일과 비밀번호가 유효한지 확인한다.
  • ``Line 19`` : 기존 세션이 있으면 해당 세션을 반환하고, 없으면 새로운 세션을 생성한다.
  • ``Line 20`` : 로그인한 사용자의 id를 세션에 저장한다.
  • ``Line 23~27`` : 수동으로 세션 ID 쿠키를 설정하는 코드
    • Spring Boot는 세션을 생성할 때 자동으로 Set-Cookie : JESSIONID = <sessionId>를 응답 헤더에 추가해준다고 한다.
    • 세션 쿠키를 커스터마이징 해야 할 경우 수동으로 설정하면 된다.

로그아웃

  • ``Line 46`` : 기존 세션이 있으면 해당 세션을 반환하고, 없으면 null을 반환한다.
  • ``Line 47~49`` : 세션이 있는 경우 세션을 삭제한다.  → 로그아웃 처리
  • ``Line 52~55`` : 세션 ID 쿠키를 삭제한다.
    • 쿠키 삭제는 수동으로 구현해야 함

LoginCheckFilter

@Slf4j
public class LoginCheckFilter implements Filter {

    private static final String[] WHITE_LIST = {
            "/", "/api/users/register", "/api/auth/login","/api/auth/logout"
    };

    @Override
    public void doFilter(
            ServletRequest request,
            ServletResponse response,
            FilterChain chain
    ) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if(!isWhiteList(requestURI)) { // 로그인 필수 URI
            HttpSession session = httpRequest.getSession(false);

            if(session == null || session.getAttribute("user") == null) {
                httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                httpResponse.getWriter().write("Authentication required");
                return;
            }
        }

        chain.doFilter(request,response);
    }

    private boolean isWhiteList(String requestURI) {
        return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
    }
}
  • `Filter` 인터페이스를 구현하여 `LoginCheckFilter` 클래스를 작성하였다.
  • `RequestURI`를 가져와서 로그인 필수인 UIR의 경우 
    • session을 가져오고
    • session이 없거나 유효하지 않을 경우 `UNAUTHORIZED` 에러를 응답하도록 한다.
  • 로그인이 필요하지 않은 URI 요청이거나 로그인 인증에 성공한 경우 `chain.doFilter(request,response);`를 통해 다음 필터를 실행하고, Controller로 접근 가능하도록 한다.

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean<LoginCheckFilter> loginCheckFilter() {
        FilterRegistrationBean<LoginCheckFilter> registrationBean
                = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LoginCheckFilter());
        registrationBean.setOrder(1);
        registrationBean.addUrlPatterns("/*");

        return registrationBean;
    }
}
  • configuration에서 빈으로 등록해주면 완료!

API TEST

로그인하지 않은 경우 `GET - /api/todos` 요청

 

로그인

로그인 후 `GET - /api/todos` 재요청

로그아웃

로그아웃 후 `GET - /api/todos` 재요청

 

완료 !