일반적인 웹 사이트 구조를 생각해보자.
로그인 하기 이전에는 ID/PW 입력창이 출력되고, 일부 메뉴는 접근이 안된다.
로그인 이후에는 {userName}님, 로그아웃 처럼, 로그인이 됐음을 알 수 있다.
사용자의 계정 정보를 입력받아 검증하고, 로그인을 유지하는 방법에 대해 알아보자.
쿠키는 클라이언트에서 보관하는 데이터이다.
클라이언트가 서버로 로그인 요청을 보낸다.
서버에서는 로그인에 성공하면 응답 헤더에 Set-Cookie: memberId=1
을 포함하여 응답한다.
클라이언트는 그 쿠키를 저장하고, 앞으로 발생하는 요청에 매번 함께 전달하게된다.
서버에서 쿠키를 함께 전달하고 싶다면, 파라미터에 HttpServletResponse
객체를 추가하고 addCookie()
메소드를 통해 응답값에 쿠키를 추가할 수 있다.
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
@CookieValue
어노테이션을 통해, 클라이언트에서 전달한 쿠키를 쉽게 사용할 수 있다.
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
@GetMapping("/")
public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {
if (memberId == null) {
return "home";
}
//로그인
Member loginMember = memberRepository.findById(memberId);
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
}
Cookie를 즉시 만료시킨다.
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
쿠키는 클라이언트에서 보관하는 데이터이기 때문에, 보안에 문제가 따른다.
이러한 문제를 해결하기 위해,
-> 위 해결 방식이 세션 동작 방식이다.
사용자가 로그인을 시도한다.
로그인에 성공하면 서버에서는 토큰 (추측 불가능한 랜덤값)을 발급하여 서버 메모리에 저장한다.
이후 토큰을 클라이언트의 쿠키에 전달한다.
클라이언트는 앞으로 토큰을 서버에 보내고, 서버는 메모리에 저장된 <토큰:유저정보>에서 매핑된 정보를 찾아서 사용한다.
쿠키 방식과 비교했을 때, 세션은 직접적인 유저 정보를 주고받지 않는다.
세션 기능을 제공하기 위해서는, 3가지 기능이 필요하다.
package hello.login.web.session;
import org.springframework.stereotype.Component;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 세션 관리
*/
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
/**
* 세션 생성
*/
public void createSession(Object value, HttpServletResponse response) {
//세션 id를 생성하고, 값을 세션에 저장
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
//쿠키 생성
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
}
/**
* 세션 조회
*/
public Object getSession(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie == null) {
return null;
}
return sessionStore.get(sessionCookie.getValue());
}
/**
* 세션 만료
*/
public void expire(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie != null) {
sessionStore.remove(sessionCookie.getValue());
}
}
private Cookie findCookie(HttpServletRequest request, String cookieName) {
if (request.getCookies() == null) {
return null;
}
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
}
ConcurrentHashMap
: HashMap 은 동시 요청에 안전하지 않다.
동시 요청에 안전한 ConcurrentHashMap 를 사용했다.
private final SessionManager sessionManager;
@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
세션은 뭔가 특별한 것이 아니라, 단지 쿠키를 사용하는데 서버에서 데이터를 유지하는 방법일 뿐이다. 그런데 프로젝트마다 세션 개념을 구현하기에는 불편하기 때문에, 서블릿에서도 세션 기능을 제공한다.
서블릿에서 제공하는 (잘 만들어진) 세션 기능을 활용해보자.
서블릿에서는 세션 기능을 HttpSession
객체로 제공한다.
쿠키의 key를 JSESSIONID로 제공하며, value는 추측할 수 없는 랜덤값이다.
Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession(); //세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
request.getSession(true) (=request.getSession())
세션이 있으면 기존 세션을 반환하고, 없으면 새로운 세션을 생성해서 반환한다.
request.getSession(false)
세션이 있으면 기존 세션을 반환하고, 없으면 새로운 세션을 생성하지 않고 null 을 반환한다.
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
//세션을 삭제한다.
HttpSession session = request.getSession(false); if (session != null) {
session.invalidate();
}
return "redirect:/";
}
session.invalidate() 를 통해 손쉽게 세션을 제거할 수 있다.
@SessionAttribute
어노테이션을 통해, 세션에 있는 값을 쉽게 가져올 수 있다.
request.getSession()과는 다르게, 세션이 없다고 새로운 세션을 생성하지 않는다.
즉, request.getSession(false)와 동일하다.
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)
Member loginMember,
Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
서블릿의 http 세션 방식을 사용하게되면, url에 jsessionid QueryString이 포함되는것을 볼 수 있다.
http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
이는 쿠키를 사용하지 않는 브라우저를 지원하기 위한 방식이다.
URL전달 방식이 아닌, 쿠키만을 사용하고 싶다면 아래와 같은 설정을 사용할 수 있다.
server.servlet.session.tracking-modes=cookie
@GetMapping("/session-info")
public String sessionInfo(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return "세션이 없습니다.";
}
//세션 데이터 출력
session.getAttributeNames().asIterator()
.forEachRemaining(name ->
log.info("session name={}, value={}", name, session.getAttribute(name)));
log.info("sessionId={}", session.getId());
log.info("maxInactiveInterval={}", session.getMaxInactiveInterval());
log.info("creationTime={}", new Date(session.getCreationTime()));
log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));
log.info("isNew={}", session.isNew());
return "세션 출력";
}
세션은 위에서 알아본대로, session.invalidate()
를 통해 제거할 수 있다.
하지만, 대부분의 사용자들은 페이지에서 로그아웃 버튼을 누르는 것이 아니라
그냥 웹페이지를 종료한다.
비연결성인 http의 특성상, 세션 정보가 서버에 계속 남아있게되며 보안에 취약해진다.
이를 통해, 일정 시간(대체적으로 30분)이 지나면 세션이 제거되도록 설정할 수 있다.
하지만, 고정된 시간동안만 세션이 유지되면, 그 시간마다 사용자가 로그인을 다시 해야하는 불편함이 생긴다. 이는 마지막으로 Session을 요청한지 n분이 지난 이후에 만료
를 통해 유지할 수 있고, 서블릿에서 지원하는 HttpSession
도 이와 같은 방식을 사용한다.
세션 타임아웃을 설정하기 위해서는 2가지 방법이 있다.
server.servlet.session.timeout=60
로 설정한다.session.setMaxInactiveInterval(1800);
//1800초필터와 인터셉터는 이전 포스팅에서도 한번 다뤘지만, 다시 한번 복습하고 간다.
필터와 인터셉터는 컨트롤러로 요청이 들어가기 전에 공통적으로 수행하고자 하는 로직을 반영할 수 있다. 즉, 모든 로직에서의 공통 관심사를 해결할 수 있다.
동일한 역할을 하는것 같지만, 차이점도 존재한다.
우선, 아래에서 동작 흐름의 차이부터 살펴보자.
필터
HTTP 요청 -> WAS-> 필터 -> 서블릿 -> 컨트롤러
인터셉터
HTTP 요청 ->WAS-> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
필터
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자
인터셉터
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비 로그인 사용자
필터
HTTP 요청 ->WAS-> 필터1-> 필터2-> 필터3-> 서블릿 -> 컨트롤러
인터셉터
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() {}
}
필터 인터페이스를 구현하고 @Configuration
으로 등록하고 나면,
서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.
init()
: 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
doFilter()
: 필터의 로직을 구현, 고객의 요청이 올 때 마다 호출된다.
destroy()
: 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
@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 {
HttpServletRequest httpRequest = (HttpServletRequest) request;
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");
}
}
doFilter(...)
HTTP 요청이 올 때마다 실행된다.
ServletRequest는 HTTP 요청이 아닌 경우까지 고려하여 만든 인터페이스이다.
HTTP 요청에서 사용하려면, 위 예제와 같이 HttpServletReques로 다운캐스팅 하여 사용할 수 있다.
chain.doFilter(request, response);
이 부분이 가장 중요하다.
다음에 수행할 필터가 존재하면 호출하고, 없으면 서블릿을 호출한다.
doFilter()를 호출하지 않으면, 더 이상 넘어가지 않고 요청이 종료된다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
setFilter(...)
: 등록할 필터를 지정한다.
setOrder(...)
: 필터는 체인으로 동작하므로, 순서가 필요하다. 낮을 수록 먼저 동작한다.
addUrlPatterns("/*")
: 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.
@ServletComponentScan
@WebFilter(filterName = "logFilter", urlPatterns = "/*")
@Slf4j
public class LogFilter implements Filter {
...
}
와 같은 방법으로, @ServletComponentScan, @WebFilter 어노테이션을 사용해서도
필터를 등록할 수 있지만, 순서 (Order) 를 설정할 수 없다.
그냥 FilterRegistrationBean 을 사용하자.
hello.login.web.filter.LogFilter: REQUEST [0a2249f2-cc70-4db4-98d1-492ccf5629dd][/items]
hello.login.web.filter.LogFilter: RESPONSE [0a2249f2-cc70-4db4-98d1-492ccf5629dd][/items]
필터를 등록할 때 urlPattern 을 /* 로 등록했기 때문에 모든 요청에 해당 필터가 적용된다.
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout","/css/*"};
@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);
//로그인으로 redirect
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return; //여기가 중요, 미인증 사용자는 다음으로 진행하지 않고 끝!
}
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e; //예외 로깅 가능 하지만, 톰캣까지 예외를 보내주어야 함
} finally {
log.info("인증 체크 필터 종료 {}", requestURI); }
}
}
/**
* 화이트 리스트의 경우 인증 체크X
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
isLoginCheckPath(requestURI)
로그인을 하지 않더라도, 접근이 가능한 페이지가 있다. (홈, 로그인, 회원가입, css, 리소스 등)
이러한 경로를 화이트리스트로 관리하여 권한 검사를 하지 않도록 한다.
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
인증되지 않은 사용자를 로그인 화면으로 보낸다.
이후에, 로그인에 성공하면 마지막으로 보던 페이지로 돌아가도록 지원한다.
(/login API에 별도 구현이 필요하다.)
return;
인증되지 않은 사용자는 서블릿, 컨트롤러까지 로직을 수행하지 않고 먼저 종료시킨다.
바로 윗줄에서 httpResponse.sendRedirect(...)
를 통해 페이지를 이동시키고 로직이 종료된다.
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
setOrder(2)
이전에 Request 로깅 필터를 1번 순서로 했으므로,
로깅 이후에 인증 로직이 수행되도록 2번 순서로 지정한다.
addUrlPatterns("/*")
모든 요청에 로그인 필터를 적용한다.
화이트리스트로 관리되는 URL은 LoginCheckFilter 로직 내에서 담당한다.
스프링의 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.
public interface HandlerInterceptor {
// 컨트롤러 호출 전
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {}
// 컨트롤러 호출 후
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {}
// 요청 완료 이후
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}
}
서블릿 필터는 doFilter()만 제공하는 반면,
인터셉터는 위와 같이 단계적으로 잘 세분화 되어 있다.
서블릿 필터는 경우 단순히 request, response 만 제공했지만,
인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보도 받을 수 있다.
그리고 어떤 modelAndView 가 반환되는지 응답 정보도 받을 수 있다.
일반적인 호출 흐름
예외(Exception)이 발생했을 때의 호출 흐름컨트롤러에서 예외가 발생하면,
preHandle은 컨트롤러 호출 전에 수행되기 때문에, 항상 수행된다.
postHandle은 호출되지 않는다.
afterCompletion은 항상 호출된다. (예외가 발생하면 ex정보와 함께 호출됨)
package hello.login.web.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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; //false를 반환하면 이후는 진행X
}
@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 requestURI = request.getRequestURI();
String logId = (String)request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}]", logId, requestURI);
// 컨트롤러에서 예외가 발생하면 ex변수가 함께 넘어온다.
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
String uuid = UUID.randomUUID().toString()
요청 로그를 구분하기 위한 uuid 를 생성한다.
request.setAttribute(LOG_ID, uuid)
서블릿 필터는
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
...
String uuid = /*...*/;
try {
log.info("REQUEST [{}][{}]", uuid, requestURI);
chain.doFilter(request, response);
} finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
와 같이 Request Log -> Controller -> Response Log
가 모두 doFilter() 안에 들어있다. 따라서 uuid를 지역변수로 사용할 수 있다.
하지만 스프링 인터셉터는 컨트롤러 호출 전/후/요청완료 이후 시점이 나뉘어져있고, 싱글톤으로 관리되기 때문에 멤버변수로 사용할 수도 없다.
따라서 uuid를 request객체에 보관하고, 이후에 afterCompletion에서 꺼내쓴다.
return true;
true 면 정상 호출이다. 다음 인터셉터나 컨트롤러가 호출된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
//...
}
스프링 인터셉터는 서블릿 필터(FilterRegistrationBean)와 다르게, 별도 빈을 등록하지 않는다.
대신,WebMvcConfigurer
를 상속받고 addInterceptors
메소드를 오버라이드 한다.
excludePathPatterns("/css/**", "/*.ico", "/error")
화이트리스트를 doFilter 로직에서 관리하던 서블릿 필터와 달리, 스프링 인터셉터는 등록 시점에 지정할 수 있다. 또한, 보다 정교하게 지정할 수도 있다.
PathPatterns : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html
REQUEST [6234a913-f24f-461f-a9e1-85f153b3c8b2][/members/add]
[hello.login.web.member.MemberController#addForm(Member)]
postHandle [ModelAndView [view="members/addMemberForm"; model={member=Member(id=null, loginId=null, name=null, password=null),org.springframework.validation.BindingResult.member=org.springframework.validation.BeanPropertyBindingResult: 0 errors}]]
RESPONSE [6234a913-f24f-461f-a9e1-85f153b3c8b2][/members/add]
package hello.login.web.interceptor;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@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(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI); return false;
}
return true;
}
}
서블릿 필터와 비교했을 때 코드가 간결해졌다.
인증은 컨트롤러 호출 전에만 수행되면 되기 때문에, preHandle(...)
만 구현하면 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/", "/members/add", "/login", "/logout", "/css/**", "/*.ico", "/error");
}
//...
}
Reference
https://stackoverflow.com/questions/47091717/interceptor-in-spring-5-webflux