이전까지 로그인 기능 자체는 구현했지만 아직까지 구현되지 않은 부분이 있다
URL에서 /items
로 접근하면 로그인 된 사용자만 접근 가능하도록 구현을 변경해야하는데/items
상에 검증 로직을 적용하지 않았기 때문에 로그인 여부와 관계없이 정상적으로 접속되는 것을 알 수 있다.
컨트롤러상에서 이 기능을 구현하려면 전체 컨트롤러 메서드마다 검증 로직을 구현해야하는데
중복되는 코드가 많아 코드 복잡성이 올라간다
이러한 복잡성을 완화하기 위해 서블릿은 필터 기능을, 스프링 MVC는 인터셉터 기능을 제공한다.
공통 관심사의 경우 이전에 공부한 스프링 AOP로도 구현할 수 있으나 이 기능은 웹과 관련된 내용이기 때문에 MVC 상에서 해결하는게 좋다.
서블릿 필터
필터는 서블릿이 제공하는 수문장과 비슷한데 전체적인 실행 흐름은 다음과 같다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
고객의 요청을 남기는 로깅 요구사항이 있는 경우 필터를 사용하는데 필터는 특정 URL 패턴에 사용할 수 있다.
/*
를 사용하면 모든 요청에 대해 필터가 적용된다.
필터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 // 로그인한 사용자
HTTP 요청 -> WAS -> 필터 (적절하지 않은 요청으로 판단하여 서블릿 호출 X)
redirect
는 새로운 요청으로 친다 따라서 적절하지 않은 요청으로 서블릿은 호출되지 않지만
새로운 요청으로 들어가게 된다!
(헷갈려서 질문한...)
필터 체인
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 서블릿 -> 컨트롤러
필터는 체인 형식이라 중간에 필터의 자유로운 추가가 가능하다.
따라서 먼저 로깅에 관한 필터를 적용 후 로그인에 관한 검증을 진행하는 필터의 적용이 가능하다.
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
필터 인터페이스의 구현과 등록은 서블릿 컨테이너가 싱글톤 객체로 생성하고, 관리한다.
init()
: 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출됨doFilter()
: 고객의 요청이 올 때마다 해당 메서드 호출됨, 필터의 주 로직 구현부destroy()
: 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
서블릿 필터 - 요청 로그 남기도록 구현
package hello.login.web.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("do log filter doFilter");
// 고객의 요청이 올때마다 doFilter 호출
// HttpServletRequest의 부모인 ServletRequest를 호출하나 기능이 거의 없어 다운캐스팅할것
HttpServletRequest httpRequest = (HttpServletRequest) request;
// Http요청말고 다른것도 받을 수 있도록 설계되었는데 다른거 안써서 다운캐스트 하는게 낫다
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try{
log.info("REQUEST = [{}], [{}]", uuid, requestURI);
chain.doFilter(request, response); // 있으면 다음 필터 없으면 서블릿 호출
} catch (Exception e){
throw e;
}
finally {
log.info("RESPONSE = [{}] [{}]", uuid, requestURI); // 여기서 컨트롤러 왔다갔다 하는 시간 측정 가능
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
@Slf4j
로그를 남기기 위해 다음과 같은 애노테이션을 클래스 단 위에 기술하였다
public class LogFilter implements Filter
- 필터로 사용하기 위해서는 필터 인터페이스의 구현이 필요하다
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- HTTP 요청이 오면
doFilter()
가 호출되는데,ServletRequest, Response
의 경우 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이기 때문에, HTTP를 사용하는 경우HttpServletRequest
로 다운캐스팅 하면 HTTP 편의 기능을 자유롭게 이용할 수 있다.String uuid = UUID.randomUUID().toString()
- HTTP 요청을 구분하기 위해 다음과 같이 임의의 uuid를 생성해두면 요청별로 어떤 로그를 남기고 있는지 구분하는 구분자로 사용할 수 있다.
log.info("Request [{}] [{}]", uuid, request)
uuid
와requestURI
를 출력한다,
chain.doFilter(request, response)
- 이 부분이 가장 중요하다. 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출하는데
이 부분이 진행되지 않게 되면 다음 단계로 진행되지 않는다.
WebConfig에 필터 등록하기
package hello.login.domain;
import hello.login.web.argumentresolver.LoginMemberArgumentResolver;
import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import hello.login.web.interceptor.LogInterceptor;
import hello.login.web.interceptor.LoginCheckInterceptor;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.Filter;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean // WAS 띄울때 필터 주입해줌
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterFilterRegistrationBean
= new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter());
filterFilterRegistrationBean.setOrder(1);
filterFilterRegistrationBean.addUrlPatterns("/*");
// 여러개 패턴 넣을 수 있는데 /*로하면 모든 url에 다 적용됨
return filterFilterRegistrationBean;
}
다음과 같은 방식으로 WebMvcConfigurer
의 구현체인 WebConfig
에 필터를 등록한다.
-> 직접 스프링 빈으로 등록해도 되는데 다만, 대부분의 경우 WebMvcConfigurer
나 SecurityConfigurer
등으로 각 설정 범위에 따라 클래스를 분리해서 설정 정보를 기입하는 경우가 많다고 한다.
setFilter(new LogFilter)
- 등록할 필터를 지정한다.setOrder(1)
- 필터는 체인으로 연쇄적으로 동작하기 때문에 어떤 필터를 우선해서 실행할지 순서를 지정하는것이 중요하다. 따라서 다음과 같은 방법으로 필터의 순서를 지정해줄 수 있다.addUrlPatterns("/*")
- 다음과 같은 방법으로 URL 패턴을 지정해줄 수 있는데 한번에 여러 패턴을 지정할 수 있다. 다만 이 방법의 경우 설정 파일을 따로 건드려줘야하기 때문에 뒤에가서 HttpServletRequest.getRequestURI()
를 이용해서 whitelist
를 스프링 필터 내에 구현한다.
애노테이션 써도 되는데 순서 지정이 안된다, FilterRegistrationBean
을 사용하는게 좋다.
서블릿 필터 - 인증체크
이제 웹사이트 접근시 로그인 되어있는지를 확인하는 필터를 구현하겠다
package hello.login.web.filter;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String whiteList[]
= { "/" ,"/login", "/members/add", "/logout", "/css/*"};
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
@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;
try{
log.info("인증 체크 필터 시작 {}" , requestURI);
if(isLoginCheckPath(requestURI)){
log.info("인증 체크 로직 실행 = {}", requestURI);
HttpSession session = httpRequest.getSession(false);
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
log.info("미인증 사용자 요청 = {}", requestURI);
// 로그인으로 리다이렉트
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
// 로그인을 했으면 다시 원래 가려했던 페이지로 왔으면 좋겠다는 뜻임
return; // 다음 서블릿이나 호출 안한다 -> sendRedirect로 보냄
}
}
chain.doFilter(request, response);
} catch (Exception e){
throw e; // 예외 로깅 가능한데 톰캣까지 예외를 올려줘야 함
// WAS 까지 안가는 경우, 서블릿 필터에서 올라오는걸 먹으면 정상처럼 동작함.
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
/**
* Whitelist Validation Override */
private boolean isLoginCheckPath(String requestURI){
return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
}
}
먼저 whiteList
배열을 통해, 이 필터가 적용되지 아니할 페이지 목록을 설정한다.
isLoginCheckPath(requestURI)
의 경우 화이트리스트를 제외한 모든 경우에 인증 체크 로직을 적용하도록 만들었다. PatternMatchUtils
의 경우 사용한 이유는 저걸 사용 안하면 requestURI
에 whiteList
에 기술된 특정 패턴이 있는지 검사하기 어렵기 때문이다.
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
미인증 사용자는 로그인 화면으로 리다이렉트하는데, 로그인에 성공하면 원래 가려했던 페이지로 돌아가게 끔requestURI
를 /login
의 파라미터로 함께 전달한다.
return;
의 경우, 필터를 더 이상 진행하지 않겠다는 뜻이다, 서블릿, 컨트롤러가 호출되지 않는데 이 경우redirect
를 사용했기 때문에 redirect
가 응답으로 적용되고 요청이 끝난다.
인증 체크 필터 등록
이전에 LogFilter
를 등록한것과 같은 방법으로 등록한다.
@Bean
public FilterRegistrationBean loginCheckFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
// 메서드 하나 호출하는거 정도는 성능 저하에 거~의 미미하다.
//WhiteList 여기서도 추가할 수 있는데 미래에 뭐가 따로 만들어져도
// 설정 파일을 건드리고 싶지 않아서 여기서는 따로 설정 안함
return filterRegistrationBean;
}
Redirect 처리 LoginController
이전에 필터에서 이용하려던 웹사이트 URL로 자동 리다이렉트 시키기 위해 requestURI
를 /login
의 쿼리 파라미터로 함께 전송했다. 이 쿼리 파라미터 처리를 위해 다음과 같이 LoginController
를 수정한다.
@PostMapping("/login")
public String loginFormV4(@Validated LoginForm loginForm,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 처리 TODO 쿠키를 만들어서 브라우저로 전송하면 브라우저에서 요청시마다 쿠키를 함께 전달함
// 스프링에서 제공하는 HTTP 세션 매니저 사용
HttpSession session = request.getSession(); // 세션이 있으면 있는 세션 반환 없으면 새로 만들어서 반환함
// 세션에 로그인 회원 정보를 보관한다.
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
// 기본적으로 다 메모리에 저장됨
// 세션 생성하려면 .getSession에 파라미터를 true(근데 디폴트라 생략가능)
// false라고 하면 새로운 세션을 반환하지 않고 null로 반환한다.
//sessionManager.createSession(loginMember, response);
return "redirect:" + redirectURL;
}
맨 마지막 줄 return
문과 @RequestParam
애노테이션을 parameter로 추가했다.
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 2편' 카테고리의 다른 글
[스프링 웹 MVC 2편] 012. 예외 처리 (0) | 2023.08.20 |
---|---|
[스프링 웹 MVC 2편] 011. 로그인 처리 - 인터셉터 (0) | 2023.08.18 |
[스프링 웹 MVC 2편] 009. 로그인 처리 - 서블릿 HTTP 세션 (0) | 2023.08.17 |
[스프링 웹 MVC 2편] 008. 로그인 처리 - 쿠키. 세션 (0) | 2023.08.16 |
[스프링 웹 MVC 2편] 007. Bean Validation (0) | 2023.08.15 |