요구사항을 보면 로그인 한 사용자만 게시글 작성, 상세 조회, 수정, 삭제가 가능하다.
여러 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만, 등록, 수정, 삭제, 조회 등등 컨트롤러의 여러 로직에 공통으로 로그인 여부를 확인해야 한다. 더 큰 문제는 향후 로그인과 관련된 로직이 변경될 때 이다. 작성한 모든 로직을 다 수정해야 할 수 있다.
이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern)라고 한다. 여기서는 등록, 수정, 삭제, 조회 등등 여러 로직에서 공통으로 인증에 대해서 관심을 가지고 있다.
이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다. 웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest
를 제공한다.
서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다. 둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용 방법이 다르다. 스프링 인터셉터가 훨씬 더 많은 기능을 제공한다.
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 된다. 스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다.
스프링 인터셉터를 사용하여 로그인 인증 체크 기능을 개발한다.
LoginCheckInterceptor
package hello.board.interceptor;
import hello.board.controller.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_USER) == null) {
log.info("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
HandlerInterceptor
인터페이스를 구현하면 된다.preHandle
만 구현하면 된다.response.sendRedirect("/login?redirectURL=" + requestURI);
requestURI
를 /login
에 쿼리 파라미터로 함께 전달한다. 물론 /login
컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능은 추가로 개발해야 한다.Location: http://localhost:8080/login?redirectURL=/board/1
return false
: 인터셉터나 컨트롤러가 더는 호출되지 않는다. 앞서 redirect 를 사용했기 때문에 redirect 가 응답으로 적용되고 요청이 끝난다.Ctrl + o → 인터페이스에서 오버라이드할 메서드 선택
WebConfig
package hello.board;
import hello.board.interceptor.LoginCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/", "/signup", "/login", "/logout", "/board",
"/*.ico", "/css/**", "/error");
}
}
WebMvcConfigurer
가 제공하는 addInterceptors()
를 사용해서 인터셉터를 등록할 수 있다.
registry.addInterceptor(new LoginCheckInterceptor())
: 인터셉터를 등록한다.order(1)
: 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.addPathPatterns()
: 인터셉터를 적용할 URL 패턴을 지정한다.excludePathPatterns()
: 인터셉터에서 제외할 패턴을 지정한다.인터셉터를 적용하거나 하지 않을 부분은 addPathPatterns
와 excludePathPatterns
에 작성하면 된다.
/**
), 홈( /
), 회원가입( /signup
), 로그인( /login
), 로그아웃 (/logout
), 게시글 조회( /board
), 리소스 조회( /css/**
), 오류( /error
)와 같은 부분은 로그인 체크 인터셉터를 적용하지 않는다.스프링의 URL 경로
스프링이 제공하는 URL 경로는 서블릿 기술이 제공하는 URL 경로와 완전히 다르다. 더욱 자세하고,
세밀하게 설정할 수 있다.
자세한 내용은 다음을 참고하자.
PathPattern 공식 문서? 한 문자 일치 * 경로(/) 안에서 0개 이상의 문자 일치 ** 경로 끝까지 0개 이상의 경로(/) 일치 {spring} 경로(/)와 일치하고 spring이라는 변수로 캡처 {spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring" {spring:[a-z]+} regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처 {*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처
LoginController
@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginForm") UserLoginForm loginForm, BindingResult bindingResult,
HttpServletRequest request,
@RequestParam(defaultValue = "/") String redirectURL) {
if (bindingResult.hasErrors()) {
log.info("errors = {}", bindingResult);
return "login/loginForm";
}
User loginUser = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
if (loginUser == null) {
log.info("login Fail");
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공
//세션이 있으면 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_USER, loginUser);
return "redirect:" + redirectURL;
}
/login
에 redirectURL
요청 파라미터를 추가해서 요청했다. 이 값을 사용해서 로그인 성공시 해당 경로로 redirect 한다.http://localhost:8080/login?redirectURL=/board/1
여기까지 SpringBoot, Gradle, JPA, MySQL, Thymeleaf를 이용하여 간단한 로그인 기능과 게시판 CRUD 기능을 구현하였다.
첫 프로젝트라 디테일한 부분도 부족하고, 강의에서 배운 내용을 활용하고 CRUD 기능을 구현하는데 초점을 맞췄기 때문에 복잡한 기능은 구현하지 않았다.
이 프로젝트에서 추가적으로 기능을 더 구현할 수도 있고, 다른 프로젝트(팀 프로젝트나 클론 코딩)를 시작할 예정이다.