스프링 부트 게시판 프로젝트 - 11 | 로그인 인증 체크

seren-dev·2022년 8월 31일
0

공통 관심 사항

요구사항을 보면 로그인 한 사용자만 게시글 작성, 상세 조회, 수정, 삭제가 가능하다.
여러 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만, 등록, 수정, 삭제, 조회 등등 컨트롤러의 여러 로직에 공통으로 로그인 여부를 확인해야 한다. 더 큰 문제는 향후 로그인과 관련된 로직이 변경될 때 이다. 작성한 모든 로직을 다 수정해야 할 수 있다.
이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(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 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능은 추가로 개발해야 한다.
    • Status Code: 302
    • 응답 헤더 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() : 인터셉터에서 제외할 패턴을 지정한다.

인터셉터를 적용하거나 하지 않을 부분은 addPathPatternsexcludePathPatterns에 작성하면 된다.

  • 기본적으로 모든 경로에 해당 인터셉터를 적용하되 ( /** ), 홈( /), 회원가입( /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이라는 변수로 캡처

링크: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html

로그인 컨트롤러 수정

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;
    }
  • 로그인에 성공하면 처음 요청한 URL로 이동
  • 로그인 체크 인터셉터에서, 미인증 사용자는 요청 경로를 포함해서 /loginredirectURL 요청 파라미터를 추가해서 요청했다. 이 값을 사용해서 로그인 성공시 해당 경로로 redirect 한다.
  • 로그인 리다이렉트 결과
    URL : http://localhost:8080/login?redirectURL=/board/1

마무리

여기까지 SpringBoot, Gradle, JPA, MySQL, Thymeleaf를 이용하여 간단한 로그인 기능과 게시판 CRUD 기능을 구현하였다.
첫 프로젝트라 디테일한 부분도 부족하고, 강의에서 배운 내용을 활용하고 CRUD 기능을 구현하는데 초점을 맞췄기 때문에 복잡한 기능은 구현하지 않았다.
이 프로젝트에서 추가적으로 기능을 더 구현할 수도 있고, 다른 프로젝트(팀 프로젝트나 클론 코딩)를 시작할 예정이다.

프로젝트 깃헙 링크

0개의 댓글