질문, 피드백 등 모든 댓글 환영합니다.
이번 시간에는 스프링 인터셉터를 활용하여 로그인 하지 않은 사용자가 todo 메인페이지, todo 생성, 수정, 삭제 uri에 접근을 막고 로그인페이지로 이동시키는 기능을 개발합니다. 또한 todo의 수정, 삭제 요청 시 존재하지 않는 todo를 호출하거나 다른 사용자의 todo에 접근할 수 없도록합니다.
또한 실수로 빼먹고 넘어갔던 로그아웃 기능을 개발합니다.
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("loginMember") == null) {
response.sendRedirect("/login?redirectURL="+requestURI);
return false;
}
return true;
}
}
컨트롤러(정확히는 핸들러 어댑터)전에 호출되는 preHandle을 오버라이딩하여 세션쿠키 검증
현재 로그인 되어있지 않은 경우 로그인 url로 리다이렉트
로그인 후 접근을 시도한 uri로 이동할 수 있도록 uri를 쿼리 파라미터에 포함시킴
LoginController
public class LoginController {
@PostMapping("/login")
public String login(@Validated @ModelAttribute LoginDto loginDto, BindingResult bindingResult,
@RequestParam(defaultValue = "/todo") String redirectURL,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "/login/form";
}
Optional<Member> loginMember = loginService.login(loginDto.getLoginId(), loginDto.getPassword());
if (loginMember.isEmpty()) {
bindingResult.reject("loginFail", "id password 에러");
return "/login/form";
}
HttpSession session = request.getSession();
session.setAttribute("loginMember", loginMember.get());
return "redirect:" + redirectURL;
}
}
LoginInterceptor에서 추가한 리다이렉트 파라미터를 처리할 수 있도록 @RequsetParam 추가
Configurer
public class Configurer implements WebMvcConfigurer {
private final ToDoService toDoService;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/logout", "/add", "/error", "/css/**", "/js/**");
}
}
회원가입, 로그인, 로그아웃, css, js, 에러, 첫 페이지 경로에 대한 요청을 제외한 나머지 요청에 LoginInterceptor 적용
ToDoInterceptor
@RequiredArgsConstructor
public class ToDoInterceptor implements HandlerInterceptor {
private final ToDoService toDoService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
Member loginMember = (Member) request.getSession(false).getAttribute("loginMember");
int pos = requestURI.lastIndexOf("/");
Long toDoId = Long.parseLong(requestURI.substring(pos + 1));
Optional<ToDo> findById = toDoService.findById(toDoId);
if (findById.isEmpty() || !findById.get().getMember().getId().equals(loginMember.getId())) {
response.sendRedirect("/todo");
return false;
}
return true;
}
}
false를 반환하는 경우 :
1. todo가 존재하지 않는 경우
2. todo가 가진 memberId와 세션쿠키에 저장된 member의 id가 다를 경우(다른 사용자의 todo에 접근한 경우)
Configurer
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ToDoInterceptor(toDoService))
.order(2)
.addPathPatterns("/todo/update/**", "/todo/change/**", "/todo/delete/**");
}
todo의 수정, 삭제 요청에 적용
LoginController
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginDto") LoginDto loginDto, HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session != null && session.getAttribute("loginMember") != null ? "redirect:/" : "/login/form";
}
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("loginMember") != null) session.invalidate();
return "redirect:/";
}
HttpSession의 invalidate()를 이용해 쿠키를 만료(삭제)시키고 첫 페이지로 이동
로그인 사용자는 /login 경로로 접근 불가능
Logout.html
<body>
<div class="col offset-10" th:fragment="logout">
<form th:action="@{/logout}" method="post">
<button type="submit" class="mt-2 col-auto btn btn-light">로그아웃</button>
</form>
</div>
</body>
로그아웃 버튼은 여러 페이지에서 사용되므로 th:fragment 기능을 이용
th:fragment 가 적용된 태그를 다른 html에서 호출하여 사용할 수 있음
home.html
<body>
<div th:if="${session.loginMember}">
<div th:replace="~{header/logout :: logout}"></div>
</div>
<button class="w-100 btn btn-secondary btn-lg" th:unless="${session.loginMember}"
onclick="location.href='items.html'" th:onclick="|location.href='@{/login}'|"
type="button">
로그인
</button>
<button class="w-100 btn btn-secondary btn-lg" th:if="${session.loginMember}"
onclick="location.href='items.html'" th:onclick="|location.href='@{/todo}'|"
type="button">
할 일
</button>
th:replce로 th:fragment 호출 가능
로그인 여부에 따라 로그아웃 버튼 활성화
로그인 여부에 따라 로그인/할 일 버튼 출력
main.html
<body>
<div th:replace="~{header/logout :: logout}"></div>
main에 출력되는 todo를 마감일과 생성일을 기준으로 정렬하여 출력하도록 수정합니다.