[Spring] MVC3: 세션, 인터셉션, 쿠키

첫걸음 - 13

Posted by owin2828 on 2019-12-30 16:50 · 10 mins read

0. 들어가기 앞서


본 문서에서 다루는 내용은 다음과 같다.

  1. 세션
  2. 인터셉터
  3. 쿠키

쿠키와 세션?
동적인 웹페이지에서 입력 정보를 유지할 필요가 있을 때 정보를 저장하는 장소에 따라 두 가지로 분류

  1. 웹 서버에 저장하는 경우: 세션
    • 일정 시간동안 같은 사용자로부터 들어오는 일련의 요구를 하나의 상태로 보고 상태를 서버에 저장하며 유지 시킴
  2. 클라이언트에 저장하는 경우: 쿠키
    • 서버에서 생성하고 클라이언트에서 관리하며 만료시간 여부에 따라 파괴됨

데이터가 작고 중요하지 않은 데이터는 클라이언트(쿠키) 쪽에서 관리하고 나머지는 웹서버(세션)에 저장하는 것이 일반적

다음과 같은 로그인 기능을 담당하는 기본적인 파일들이 본 프로젝트에 이미 구성되어 있다(소스코드 다운).

  • AuthInfo.java
  • AuthService.java
  • LoginCommand.java
  • LoginCommandValidator.java
  • LoginController.java
  • loginFrom.jsp
  • loginSuccess.jsp

1. 컨트롤러에서 HttpSession 사용


로그인 기능을 구현 후 로그인 상태를 유지하는 방법은 크게 다음과 같이 두 가지가 존재

  1. HttpSession
  2. 쿠키

컨트롤러에서 HttpSession을 사용하려면 다음의 두 가지 방법중 한 가지를 사용

  • 요청 매핑 어노테이션 적용 매서드에 HttpSession 파라미터를 사용
      @PostMapping
      public String form(LoginCommand loginCommand, Errors errors, HttpSession session){
          ... // session을 사용하는 코드
      }
    
  • 요청 매핑 어노테이션 적용 매서드에 HttpServletRequest 파라미터를 추가하고 HttpServletRequest를 이용해 HttpSession을 구함
      @PostMapping
      public String submit(
          LoginCommand loginCommand, Errors errors, HttpServletRequest req){
        HttpSession session = req.getSession();
          ... // session을 사용하는 코드
      }
    

    첫 번째 방법은 항상 HttpSession을 생성하지만, 두 번째 방법은 필요한 시점에만 HttpSession을 생성
    두 방법 모두 기존에 존재하는 세션이 있을시, 존재하는 세션을 전달

  • 로그인을 위한 컨트롤러 클래스는 다음과 같이 인증 정보를 세션에 담음
      // LoginController.java
      @Controller
      @RequestMapping("/login")
      public class LoginController {
          ...
          @PostMapping
          public String submit(
                  LoginCommand loginCommand, Errors errors, HttpSession session,
                  HttpServletResponse response) {
              new LoginCommandValidator().validate(loginCommand, errors);
              if (errors.hasErrors()) {
                  return "login/loginForm";
              }
              try {
                  AuthInfo authInfo = authService.authenticate(
                          loginCommand.getEmail(),
                          loginCommand.getPassword());
                    
                  // 로그인에 성공 시 HttpSession의 authInfo 속성에 인증 정보 객체(authInfo)를 저장
                  session.setAttribute("authInfo", authInfo);
    
              ...
              } catch (WrongIdPasswordException e) {
                  errors.reject("idPasswordNotMatching");
                  return "login/loginForm";
              }
          }
      }  
    
  • 로그아웃을 위한 컨트롤러 클래스는 HttpSession을 다음과 같이 제거
      // LogoutController.java
      @Controller
      public class LogoutController {
    
          @RequestMapping("/logout")
          public String logout(HttpSession session) {
              session.invalidate();
              return "redirect:/main";
          }
    
      }
    


2. 인터셉터 사용


  • 현재 로그인하지 않고 바로 비밀번호를 변경하는 주소를 웹 브라우저에 입력할 경우 비밀번호 변경 폼이 출력 되는데,
    로그인하지 않고도 변경 폼이 출력되는 것은 비정상적이며 이를 방지해야 함
  • 이를 해결하기 위해 HttpSession에 authInfo 객체의 유무를 조사 후, 리다이렉트 시키는 방법으로 해결할 수 있으나,
    각 기능을 구현한 모든 컨트롤러에 이런 세션 확인 코드를 삽입하는 것은 비효율

    이렇게 다수의 컨트롤러에 대해 동일한 기능을 적용해야 할 때 사용할 수 있는 것이 HandlerInterceptor

2-1. HandlerInterceptor 구현

  • org.springframework.web.HandlerInterceptor 인터페이스를 이용해 구현하며 다음과 같은 시점에 공통 기능 삽입 가능
    1. 컨트롤러 실행 전
    2. 컨트롤러 실행 후, 아직 뷰를 실행 전
    3. 뷰를 실행한 이후
  • 이러한 시점을 처리하기 위해 HandlerInterceptor 인터페이스는 다음 매서드를 정의
    1. boolean prehandle(…)
    2. void postHandle(…)
    3. void afterCompletion(…)
  1. preHandle():
    리턴 타입은 boolean으로써, 만약 false를 리턴하게 되면 컨트롤러 또는 다음 핸들러인터셉터를 실행하지 않음
  2. postHandle():
    컨트롤러가 정상적으로 실행된 이후에 추가 기능을 구현할 때 사용하며,
    컨트롤러가 익셉션을 발생하면 postHandle() 매서드는 실행하지 않음
  3. afterCompletion():
    뷰가 클라이언트에 응답을 전송한 뒤에 실행하며,
    컨트롤러 실행 이후에 예기치 않게 발생한 익셉션 로그실행 시간을 기록하기에 적합

     // AuthCheckInterceptor.java
     public class AuthCheckInterceptor implements HandlerInterceptor {
         @Override
         public boolean preHandle(
                 HttpServletRequest request,
                 HttpServletResponse response,
                 Object handler) throws Exception {
             HttpSession session = request.getSession(false);
             if (session != null) {
                 Object authInfo = session.getAttribute("authInfo");
                 if (authInfo != null) {
                     return true;
                 }
             }
                     // 인증정보가 없어 실패 시, 다음과 같은 경로로 리다이렉트 시킴
             response.sendRedirect(request.getContextPath() + "/login");
             return false;
         }
    
     }
    


2-2. HandlerInterceptor 설정

  • HandlerInterceptor를 구현한 다음, 어디에 적용할지 설정이 다음과 같이 필요
      // MvcConfig.java
      @Configuration
      @EnableWebMvc
      public class MvcConfig implements WebMvcConfigurer {
          ...
              // 인터셉트를 정의하는 매서드
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(authCheckInterceptor())
                  .addPathPatterns("/edit/**")
                  .excludePathPatterns("/edit/help/**");
          }
          ...
      }
    
  • addInterceptor 매서드는 인터셉터를 적용할 경로 패턴을 Ant 경로 패턴을 이용하여 지정

    Ant 경로 패턴?
    Ant 패턴은 *, **, ?의 세 가지 특수 문자를 사용해 경로를 다음과 같이 표현

    • *: 0개 또는 그 이상의 글자
    • ?: 1개 글자
    • **: 0개 또는 그 이상의 폴더 경로
      따라서 앞선 코드의 경우, http://localhost:8080/sp5-chap13/edit/changePassword 에 접근하면 로그인 폼으로 리다이렉트

3. 컨트롤러에서 쿠키 사용


  • 로그인할 때 이메일을 기억하여 쿠키저장하는 방식을 구현
  • 스프링 MVC에서 쿠키를 사용하는 방법 중 하나는 @CookieValue 어노테이션을 사용하는 것
  • @CookieValue 어노테이션은 요청 매핑 어노테이션 적용 매서드의 Cookie 타입의 파라미터에 적용

      // LoginController.java
      @Controller
      @RequestMapping("/login")
      public class LoginController {
          ...
          @GetMapping
          public String form(LoginCommand loginCommand,
                  /*
                      * 어노테이션을 통해 쿠키의 이름을 REMEMBER로 지정  
                      * 지정한 이름의 쿠키가 없다면, required 속성 값을 false로 지정
                      * 만약 지정한 이름의 쿠키가 없는데, required가 ture면 익셉션 발생
                      */
                  @CookieValue(value = "REMEMBER", required = false) Cookie rCookie) {
              if (rCookie != null) {
                  loginCommand.setEmail(rCookie.getValue());
                  loginCommand.setRememberEmail(true);
              }
              return "login/loginForm";
          }
    
      /*
          * 실제 쿠키를 생성하는 부분은 로그인을 처리하는 다음 매서드
          * 쿠키를 사용하기 위해 HttpServletResponse 객체가 필요하므로 파라미터로 전달
          */
          @PostMapping
          public String submit(
                  LoginCommand loginCommand, Errors errors, HttpSession session,
                  HttpServletResponse response) {
              ...
                      // 쿠키를 추가하는 코드
              Cookie rememberCookie = 
                      new Cookie("REMEMBER", loginCommand.getEmail());
              rememberCookie.setPath("/");
                    
                  /*
                      * 로그인에 성공했을 때, 이메일 기억하기 체크박스 선택 여부에 따라
                      * 30일동안 유지되는 쿠키를 생성하거나
                      * 바로 삭제되는 쿠키를 생성
                      */
              if (loginCommand.isRememberEmail()) {
                  rememberCookie.setMaxAge(60 * 60 * 24 * 30);
              } else {
                  rememberCookie.setMaxAge(0);
              }
              response.addCookie(rememberCookie);
              ...
          }
      }