Spring boot 중고거래 쇼핑몰 사이트 만들기 프로젝트 5 (로그인 확인하기, Spring Interceptor)

전승재·2023년 8월 1일

배경

로그인한 사용자만이 상품관리, 상품수정, 상품삭제, 상품등록 등을 수행할 수 있다.
하지만 현재 로그인하지 않은 사용자가 URL을 입력해도 해당 사이트에 접속할 수 있다.
상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만, 등록, 수정, 삭제, 조회 등등 상품관리의 모든 컨트롤러 로직에 공통으로 로그인 여부를 확인해야 한다.
더 큰 문제는 향후 로그인과 관련된 로직이 변경될 때 이다. 작성한 모든 로직을 다 수정해야 할 수 있다.
이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern)라고 한다. 여기서는 등록, 수정, 삭제, 조회 등등 여러 로직에서 공통으로 인증에 대해서 관심을 가지고 있다.
웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest 를 제공한다
따라서 우리는 클라이언트가 보낸 요청을 수행하기 전에 스프링 인터셉터를 사용하여 사용자가 로그인되어 있는지를 확인해야 한다.

Interceptor filter

서블릿 필터는 서블릿에서 제공하는 기술이고 스프링 인터셉터는 스프링 MVC에서 제공하는 기술이다.
흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러

스프링 인터셉터 체인

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러

인터셉터가 여러개일 경우에는 위와 같이 동작한다.

서블릿 필터의 경우 단순하게 doFilter() 하나만 제공된다. 인터셉터는 컨트롤러 호출 전( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion )와 같이 단계적으로 잘 세분화 되어 있다

스프링 인터셉터 정상 흐름

  • preHandle : 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.)
  • preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않는다. false인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 그림에서 1번에서 끝이 나버린다.
  • postHandle : 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.)
  • afterCompletion : 뷰가 렌더링 된 이후에 호출된다

스프링 인터셉터 예외 발생시

  • preHandler: 컨트롤러 호출 전에 실행된다.
  • postHandler: 컨트롤러에서 예외가 발생한다면 호출되지 않는다.
  • afterCompletion: 예외가 발생해도 항상 호출된다.

Interceptor로 Log 확인하기 - LogInterceptor

  • preHandler : 요청 로그를 구분하기 위한 uuid를 생성한다. postHandler와 afterCompletion에서 사용하기 위해 request.setAttribute를 통해 request에 담아 로그를 출력한다.
    return true를 통해 정상 호출임을 알린다.
  • postHandler
    호출 후에 로그를 출력한다.
  • afterCompletion
    Response와 만약 예외가 발생했을 경우에 예외를 로그로 출력한다.
package com.shopingmall.seungjae.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import java.util.UUID;

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();
        request.setAttribute(LOG_ID, uuid);
        //@RequestMapping: HandlerMethod
        //정적 리소스 : ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
        }
        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true;

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle [{}]",modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String logId =(String) request.getAttribute(LOG_ID);
        String requestURI = request.getRequestURI();

        log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler);
        if (ex != null) {
            log.error("afterComplement error", ex);
        }
    }
}

로그인 확인하기 - LoginCheckInterceptor

session을 가져와 session이 비어있는지, 맞는 session인지를 확인하고 만약 아니라면 log에 '미인증 사용자 요청'을 출력하고 redirect한다.
이때 redirect하는 URL은 로그인 페이지이다.
따라서 미인증 사용자가 URL입력을 통해 인증이 필요한 페이지를 접속하려고 한다면 이 인터셉터에 의해 로그인 페이지로 돌아가게 된다.
그 후 return false를 통해 다음 인터셉터를 실행하지 않도록 한다.
하지만 인증된 사용자일 경우 return true를 통해 다음 인터셉터를 실행한다.

package com.shopingmall.seungjae.interceptor;

import com.shopingmall.seungjae.controller.Member.SessionConst;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        log.info("인증 체크 인터셉터 실행 {}", requestURI);
        HttpSession session = request.getSession();
        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info("미인증 사용자 요청");
            response.sendRedirect("/member/login?redirectURL=" + requestURI);
            return false;
        }
        return true;
    }
}

인터셉터 등록 - SpringConfig

이렇게 생성한 인터셉터를 등록해야한다.
Spring의 설정을 모아둔 SpringConfig에 WebMvcConfigurer를 implement한다.
addInterceptors를 오버라이딩하여 인터셉터를 등록한다.
.addInterceptor(new 등록할 인터셉터)
.order(순서)
.addPathPatterns("/인터셉터가 적용될 URL")
.excludePathPatterns("/인터셉터가 적용되지 않을 URL")

   @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/error"); //인터셉터 호출 안되는 URL

        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns("/","/member/**");
    }

결과

상품관리 페이지로 접속하기 위해 URL을 입력해도 다시 로그인 페이지로 돌아가고 로그인 했을 때 다시 접속했던 URL로 돌아가게 된다.
사용자 권한이 없다면 "/","/member/**" 외의 사이트에 접속할 수 없다.

0개의 댓글