세션, 인터셉터, 쿠키

HeeSeong·2021년 8월 6일
0
post-thumbnail

세션, 인터셉터, 쿠키


로그인 상태를 유지하는 방법은 크게 세션, 쿠키를 이용하는 방법이 있다.


세션


@PostMapping
public String submit(LoginCommand loginCommand, Errors errors, 
HttpSession session) {

}

요청 매핑 애노테이션 적용 메서드에 HttpSession 파라미터가 존재할 경우 스프링 MVC는 컨트롤러의 메서드를 호출할 때 HttpSession 객체를 파라미터로 전달한다. HttpSession을 생성하기 전이면 새로운 HttpSession을 생성하고 그렇지 않으면 기존에 존재하는 HttpSession을 전달한다.


@PostMapping
public String submit(LoginCommand loginCommand, Errors errors,
HttpServletRequest req) {
	HttpSession session = req.getSession();
}

전의 방법은 항상 HttpSession을 생성하지만, 이 방법은 필요한 시점에만 HttpSession을 생성할 수 있다.


    @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());
            // 세션에 정보 저장
            session.setAttribute("authInfo", authInfo);
            return "login/loginSuccess";
        } catch (WrongIdPasswordException e) {
            errors.reject("idPasswordNotMatching");
            return "login/loginForm";
        }
    }

로그인에 성공하면 HttpSession의 "authInfo" 속성에 인증 정보 객체를 저장하도록 구현한 것이다.
로그아웃을 하려면 HttpSession을 제거하면 된다.


@Controller
public class LogoutController {

	@RequestMapping("/logout")
	public String logout(HttpSession session) {
		session.invalidate();
		return "redirect:/main";
	}
}

인터셉터


로그인하지 않은 상태에서 로그인한 상태에서만 이용할 수 있는 자원에 접근할 수 있는 것은 좋지 않다.
이를 위해 HttpSession에 "authInfo" 객체가 존재하는지 검사하고 존재하지 않으면 로그인 경로로 리다이렉트하도록 할 수 있다.

그런데 실제 웹 어플리케이션에서는 많은 기능에서 로그인 여부를 확인해야 한다. 각 기능을 구현한 컨트롤러 코드마다 세션 확인 코드를 삽입하는 것은 많은 중복을 일으킨다.

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


HandlerInterceptor 인터페이스


org.springframework.web.HandlerInterceptor 인터페이스를 사용하면 세가지의 시점에 공통 기능을 넣을 수 있다.


  • 컨트롤러(핸들러) 실행 전

  • 컨트롤러 실행 후(뷰 실행 전)

  • 뷰를 실행한 후


boolean preHandle(HttpServletRequest request, HttpServletResponse response,
	Object handler) throws Exception;
    
void postHandle(HttpServletRequest request, HttpServletResponse response,
	Object handler, ModelAndView modelAndView) throws Exception;
    
void afterCompletion(HttpServletRequest request, HttpServletResponse response,
	Object handler, Exception ex) throws Exception;

preHandle() 메서드는 컨트롤러 객체를 실행하기 전에 필요한 기능을 구현할 때 사용한다. handler 파라미터는 웹 요청을 처리할 컨트롤러 객체이다. 이 메서드로 '로그인하지 않은 경우 컨트롤러 실행 안하기', '컨트롤러 실행하기 전에 필요한 정보 생성' 등을 구현할 수 있다.

preHandle() 메서드의 리턴 타입은 boolean이다. preHandle() 메서드가 false를 리턴하면 컨트롤러를 실행하지 않는다.

postHandle() 메서드는 컨트롤러가 정상적으로 실행된 이후에 추가 기능을 구현할 때 사용한다. 컨트롤러가 익셉션을 발생하면 postHandle() 메서드는 실행하지 않는다.

afterCompletion() 메서드는 뷰가 클라이언트에 응답을 전송한 뒤에 실행된다. 컨트롤러 실행 과정에서 익셉션이 발생하면 이 메서드의 네번째 파라미터로 전달된다. 익셉션이 발생하지 않으면 네번째 파라미터는 null이 된다. 따라서 컨트롤러 실행 이후에 예기치 않게 발생한 익셉션을 로그로 남긴다거나 실행 시간을 기록하는 등의 후처리에 적합한 메서드다.



HandlerInterceptor 인터페이스의 각 메서드는 아무 기능을 구현하지 않은 자바 8의 디폴트 메서드이다. 따라서 HandlerInterceptor 인터페시으 메서드를 모두 구현할 필요가 없다. 이 인터페이스를 상속받고 필요한 메서드만 정의하면 된다.

여기서 HandlerInterceptor 구현 클래스는 preHandle() 메서드를 사용한다. HttpSession에 "authInfo" 속성이 존재하지 않으면 지정한 경로로 리다이렉트 하도록 구현한다.


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;
	}

}

preHandle() 메서드는 HttpSession에 "authInfo" 속성이 존재하면 true를 리턴한다. 존재하지 않으면 리다이렉트 응답을 생성한 뒤 false를 리턴한다. true를 리턴하면 컨트롤러를 실행하므로 로그인 상태에만 컨트롤러를 실행한다. 반대로 false를 리턴하면 로그인 상태가 아니므로 지정한 경로로 리다이렉트한다.

request.getContextPath()는 현재 컨텍스트 경로를 리턴한다. 웹 어플리케이션 경로가 "http://localhost:8080/abc" 이면 컨텍스트 경로는 "/abc" 가 된다. 따라서 "/abc/login" 으로 리다이렉트하라는 응답을 전송한다.


HandlerInterceptor 설정


HandlerInterceptor를 구현하면 어디에 적용할지 설정해야 한다.


@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/main").setViewName("main");
	}
}

WebMvcConfigurer#addInterceptor() 메서드는 인터셉터를 설정하는 메서드이다.

Interceptor#addInterceptor() 메서드는 HandlerInterceptor 객체를 설정한다.

Interceptor#addInterceptor() 메서드는 InterceptorRegistration 객체를 리턴하는데, 이 객체의 addPathPattern() 메서드는 인터셉터를 적용할 경로 패턴을 지정한다. 경로는 Ant 경로 패턴을 사용한다.

Ant 패턴은 세가지 특수문자를 이용해서 경로를 표현한다.

  • '*' : 0개 또는 그 이상의 글자
  • '?' : 1개 글자
  • '**' : 0개 또는 그 이상의 폴더 경로

쿠키


사용자 편의를 위해 아이디를 기억해 두었다가 다음에 로그인할 때 아이디를 자동으로 넣어주는 사이트가 많다. 이 기능을 구현할 때 쿠키를 사용한다.


    @GetMapping
    public String form(LoginCommand loginCommand,
    	@CookieValue(value = "REMEMBER", required = false) Cookie rCookie) {
		if (rCookie != null) {
			loginCommand.setEmail(rCookie.getValue());
			loginCommand.setRememberEmail(true);
		}
    	return "login/loginForm";
    }

스프링 MVC에서 쿠키를 사용하는 방법 중 하나는 @CookieValue 애노테이션을 사용하는 것이다. @CookieValue 애노테이션은 요청 매핑 애노테이션 적용 메서드의 Cookie 타입 파라미터에 적용한다. 이를 통해 쉽게 쿠키를 Cookie 파라미터로 전달받을 수 있다.

@CookieValue 애노테이션의 value 속성은 쿠키의 이름을 지정한다. 이름이 REMEMBER인 쿠키를 Cookie 타입으로 전달받는다. 지정한 이름을 가진 쿠키가 존재하지 않을 수도 있다면 required 속성값을 false로 지정한다.

required 속성의 기본 값은 true이다. required가 true인 상태에서 지정한 이름을 가진 쿠키가 존재하지 않으면 스프링 MVC는 익셉션을 발생시킨다.


@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());
            
            session.setAttribute("authInfo", authInfo);

			Cookie rememberCookie = 
					new Cookie("REMEMBER", loginCommand.getEmail());
			rememberCookie.setPath("/");
			if (loginCommand.isRememberEmail()) {
				rememberCookie.setMaxAge(60 * 60 * 24 * 30);
			} else {
				rememberCookie.setMaxAge(0);
			}
			response.addCookie(rememberCookie);

            return "login/loginSuccess";
        } catch (WrongIdPasswordException e) {
            errors.reject("idPasswordNotMatching");
            return "login/loginForm";
        }
    }

실제로 REMEMBER 쿠키를 생성하는 부분은 로그인을 처리하는 submit() 메서드이다. 쿠키를 생성하려면 HttpServletResponse 객체가 필요하므로 submit() 메서드의 파라미터로 HttpServletResponse 타입을 추가한다.

profile
끊임없이 성장하고 싶은 개발자

0개의 댓글