Spring

[Spring] 스프링 MVC 3 : 세션, 인터셉터, 쿠키 (Session, Interceptor, Cookie)

랩실외톨이 2023. 12. 16. 21:47
반응형

 

 

 

 

이번 포스팅에서는 HttpSession, HandlerInterceptor, Cookie에 대해서 다룹니다.

 

로그인 기능으로 설명하기 위해 코드를 작성해 보겠습니다.

 

AuthInfo.java

 

package spring;

public class AuthInfo {

	private Long id;
	private String email;
	private String name;

	public AuthInfo(Long id, String email, String name) {
		this.id = id;
		this.email = email;
		this.name = name;
	}

	public Long getId() {
		return id;
	}

	public String getEmail() {
		return email;
	}

	public String getName() {
		return name;
	}

}

 

 

로그인 성공 후 인증 상태 정보를 세션에 보관할 때 사용할 클래스를 작성했습니다.

 

다음으로 암호 일치 여부를 확인하기 위해 메서드를 추가해 보겠습니다.

 

Member.java

 

public boolean matchPassword(String password) {
	return this.password.equals(password);
}

 

 

이제 이메일과 비밀번호가 일치하는지 확인하기 위해서 AuthInfo 객체를 생성하는 로직을 작성해 보겠습니다.

 

AuthService.java

 

public class AuthService {

	private MemberDao memberDao;

	public void setMemberDao(MemberDao memberDao) {
		this.memberDao = memberDao;
	}

	public AuthInfo authenticate(String email, String password) {
		Member member = memberDao.selectByEmail(email);
		if (member == null) {
			throw new WrongIdPasswordException();
		}
		if (!member.matchPassword(password)) {
			throw new WrongIdPasswordException();
		}
		return new AuthInfo(member.getId(),
				member.getEmail(),
				member.getName());
	}

}

 

 

이제 AuthService를 이용해 로그인 요청을 처리하는 컨트롤러를 작성해 보겠습니다.

이를 위해 폼에 입력한 값을 전달받기 위한 LoginCommand 클래스와,

폼에 입력된 값이 올바른지 검사하기 위한 LoginCommandValidator 클래스도 같이 작성해 보겠습니다.

 

LoginCommand.java

 

public class LoginCommand {

	private String email;
	private String password;
	private boolean rememberEmail;

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public boolean isRememberEmail() {
		return rememberEmail;
	}

	public void setRememberEmail(boolean rememberEmail) {
		this.rememberEmail = rememberEmail;
	}

}

 

 

LoginCommandValidator.java

 

public class LoginCommandValidator implements Validator {

	@Override
	public boolean supports(Class<?> clazz) {
		return LoginCommand.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "required");
		ValidationUtils.rejectIfEmpty(errors, "password", "required");
	}

}

 


LoginController.java

 

@Controller
@RequestMapping("/login")
public class LoginController {
  private AuthService authService;

  public void setAuthService(AuthService authService) {
    this.authService = authService;
  }

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

  @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());

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

 

 

로그인 폼을 보여주기 위해서 loginForm, 로그인 성공 결과를 보여주기 위해 loginSuccess 뷰를 작성해 보겠습니다.

 

loginForm.jsp

 

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<!DOCTYPE html>
<html>
<head>
  <title><spring:message code="login.title" /></title>
</head>
<body>
<form:form modelAttribute="loginCommand">
  <form:errors />
  <p>
    <label><spring:message code="email" />:<br>
      <form:input path="email" />
      <form:errors path="email"/>
    </label>
  </p>
  <p>
    <label><spring:message code="password" />:<br>
      <form:password path="password" />
      <form:errors path="password"/>
    </label>
  </p>
  <input type="submit" value="<spring:message code="login.btn" />">
</form:form>
</body>
</html>

 

 

loginSuccess.jsp

 

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<!DOCTYPE html>
<html>
<head>
  <title><spring:message code="login.title" /></title>
</head>
<body>
<p>
  <spring:message code="login.done" />
</p>
<p>
  <a href="<c:url value='/main'/>">
    [<spring:message code="go.main" />]
  </a>
</p>
</body>
</html>

 

 

그리고 뷰에서 사용할 메시지를 추가해 보겠습니다.

 

 

label.properties

 

login.title=로그인
login.btn=로그인하기
idPasswordNotMatching=아이디와 비밀번호가 일치하지 않습니다.
login.done=로그인에 성공했습니다.

 

 

이제 작성한 컨트롤러와 서비스를 설정 파일에 스프링 빈으로 등록해 보겠습니다.

 

MemberConfig.java

 

@Bean
public AuthService authService() {
	AuthService authService = new AuthService();
	authService.setMemberDao(memberDao());
	return authService;
}

 

 

ControllerConfig.java

 

@Autowired
private AuthService authService;

@Bean
public LoginController loginController() {
	LoginController controller = new LoginController();
	controller.setAuthService(authService);
	return controller;
}

 

 

그럼 이제 돌려서 코드가 잘 작성됐는지 확인해 보겠습니다.

 

 

 

 

이메일과 비밀번호가 틀린 경우에는 이렇게 메시지가 출력됩니다.

 

 

 

로그인에 성공한 경우 성공 화면이 출력됩니다.

 

 

 

 

 

반응형

 

 

 

 

 

그럼 이제 로그인 상태를 유지하기 위해서 HttpSession을 이용해 구현해 보겠습니다.

 

이를 구현하는 방법은 두 가지가 있습니다. 

 

  1. 요청 매핑 애노테이션 적용 메서드에 HttpSession 파라미터를 추가하기
  2. 요청 매핑 애노테이션 적용 메서드에 HttpServletRequest 파라미터를 추가하고 HttpServletRequest를 이용해서 HttpSession을 구하기

 

1번 버전

 

@GetMapping
public String form(LoginCommand loginCommand, Errors errors, HttpSession session) {
	...
}

 

 

파라미터를 사용해 항상 HttpSession을 생성하게 됩니다.

 

 

2번 버전

 

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

 

메서드에서 직접 코드를 작성함으로써 필요한 시점에만 HttpSession을 생성할 수 있습니다.

 

 

인증 후에 인증 정보를 세션에 담도록 submit() 메서드를 수정해 보겠습니다.

 

LoginController.java

 

  @PostMapping
  public String submit(
      LoginCommand loginCommand, Errors errors, HttpSession session) {
    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을 제거하면 세션을 종료시킬 수 있습니다. 로그아웃을 구현할 LogoutController를 작성해 보겠습니다.

 

 

LogoutController.java

 

@Controller
public class LogoutController {

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

}

 

 

새로운 컨트롤러를 구현했으므로 스프링 설정에 빈을 추가해 보겠습니다.

 

ControllerConfig.java

 

@Bean
public LogoutController logoutController() {
	return new LogoutController();
}

 

이제 HttpSession을 제대로 사용하는지 확인해 보겠습니다.

 

main.jsp

 

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <title>메인</title>
</head>
<body>
    <c:if test="${empty authInfo}">
    <p>환영합니다.</p>
    <p>
        <a href="<c:url value="/register/step1" />">[회원 가입하기]</a>
        <a href="<c:url value="/login" />">[로그인]</a>
    </p>
    </c:if>
    
    <c:if test="${! empty authInfo}">
    <p>${authInfo.name}님, 환영합니다.</p>
    <p>
        <a href="<c:url value="/edit/changePassword" />">[비밀번호 변경]</a>
        <a href="<c:url value="/logout" />">[로그아웃]</a>
    </p>
    </c:if>
</body>
</html>

 

 

로그인에 성공할 경우 HttpSession의 authInfo 속성에 인증 정보 객체를 저장하게 됩니다.

그러므로 authInfo가 true가 되기 때문에 아래의 if문이 실행되어 비밀번호 변경 / 로그아웃 문구가 출력됩니다.

 

 

 

 

 

 

 

반응형

 

 

 

 

 

그럼 이제 비밀번호 변경 기능을 구현해 보겠습니다.

 

  • ChangePwdCommand
  • ChangePwdCommandValidator
  • ChangePwdController
  • changePwdForm.jsp
  • changedPwd.jsp
  • label.peoperties
  • ControllerConfig

기능 구현을 위해서는 위의 파일을 새로 작성하거나 수정해야 합니다. 

 

spring.zip
0.00MB

 

 

첨부 파일을 참조해 코드를 작성해 주세요. 코드 내용은 앞서했던 내용의 반복이기 때문에 생략했습니다.

 

 

잘 작동합니다. 이때 코드를 작성하고 서버를 재시작하면 세션 정보가 유지되지 않기 때문에 로그인부터 다시 시작해야 합니다.

 

하지만 아직 문제가 있습니다. 만약 로그인을 하지 않은 상태에서 "http://localhost:8080/edit/changePassword" 이 경로로 들어가면 어떻게 될까요? 로그인의 유무와 상관없이 비밀번호 변경 폼이 출력됩니다.

 

이를 방지하기 위해 로그인을 하지 않았을 때 위의 주소로 접속했다면, 로그인 화면으로 이동하도록 코드를 작성해 보겠습니다.

이를 위해서는 ChangePwdController에서 로그인의 여부를 확인하는 HttpSession이 null이면 로그인하지 않은 상태이기 때문에 redirect:/login을 return 해 뷰로 이동을 시킬 수 있습니다.

 

 

 

 

 

반응형

 

 

 

 

 

하지만 실제로 웹 개발을 하게 된다면 이외에도 많은 부분에서 로그인 여부를 체크해야 합니다. 그때마다 같은 코드를 삽입하는 건 중복을 일으킵니다. 이렇게 다수의 컨트롤러에 대한 동일한 기능을 적용해야 할 때 HandlerInterceptor를 사용할 수 있습니다.

 

HandlerInterceptor 인터페이스를 사용하면 세 시점에 공통 기능을 넣을 수 있습니다.

 

  1. 컨트롤러(핸들러) 실행 전
  2. 컨트롤러(핸들러) 실행 후, 아직 뷰를 실행하기 전
  3. 뷰를 실행한 후

이 세 지점을 처리하기 위해 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 파라미터는 웹 요청을 처리할 컨트롤러(핸들러) 객체입니다.

로그인을 하지 않은 경우 컨트롤러를 실행하지 않고, 컨트롤러를 실행하기 전에 컨트롤러에서 필요로 하는 정보를 생성합니다.

메서드의 리턴 타입은 boolean이고, false를 리턴하면 컨트롤러(또는 다음 HandlerInterceptor)를 실행하지 않습니다.

 

postHandle() 메서드는 컨트롤러(핸들러)가 정상적으로 실행된 이후에 추가 기능을 구현할 때 사용합니다.

컨트롤러가 익셉션을 발생하면 postHandle() 메서드는 실행하지 않습니다.

 

afterCompletion() 메서드는 뷰가 클라이언트에 응답을 전송한 뒤에 실행됩니다.

컨트롤러 실행 과정에서 익셉션이 발생하면 이 메서드의 네 번째 파라미터로 전달됩니다.(익셉션이 발생하지 않는 경우에는 null)

따라서 컨트롤러 실행 이후 발생한 익셉션을 로그로 남기는 등 후처리를 하기 위해 적합한 메서드입니다.

 

비밀번호 변경 기능에 접근할 때 HandlerInterceptor를 사용하면 로그인 여부에 따라 로그인 폼으로 보내거나 컨트롤러를 실행하도록 구현할 수 있습니다.

preHandle() 메서드를 사용해 HttpSession에 authInfo 속성이 존재하지 않으면(세션이 없는 경우, 즉 로그인하지 않은 경우)

지정한 경로로 리다이렉트 하도록 구현하겠습니다.

 

 

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

}

 

 

HttpSession에 authInfo 속성이 존재하면 true를 리턴하고 아니면 리다이렉트 응답을 생성한 뒤 false를 리턴합니다.

즉, 로그인 상태면 컨트롤러를 실행하고 아니라면 리다이렉트를 하게 됩니다.

request.getContextPath()는 현재 콘텍스트 경로를 리턴합니다.

 

그럼 HandlerInterceptor를 어디에 적용할지 MvcConfig에서 설정해 보겠습니다.

 

@Override
public void addInterceptors(InterceptorRegistry registry) {
	registry.addInterceptor(authCheckInterceptor())
		.addPathPatterns("/edit/**");
        	.excludePathPatterns("/edit/help/**");
}

@Bean
public AuthCheckInterceptor authCheckInterceptor() {
	return new AuthCheckInterceptor();
}

 

 

authCheckInterceptor()를 인터셉터로 설정한 후, edit 패키지 밑에 있는 모든 경로에 인터셉터를 적용해 줬습니다.

addPathPatterns()으로 지정한 경로 패턴 중 일부를 제외하고 싶다면, excludePathPatterns()를 사용하면 됩니다.

실제로 적용되었는지 확인해 보겠습니다.

 

 

비밀번호 찾기 화면에서 로그인 화면으로 리다이렉트 된 모습입니다.

 

 

 

 

반응형

 

 

 

 

 

그럼 이번엔 쿠키를 사용해 보겠습니다.

쿠키는 사용자의 편의를 위해 아이디를 기억해 두었다가 다음에 로그인할 때 아이디를 자동으로 넣어주는 기능에 필요합니다.

예제에서 이 기능을 구현해 보겠습니다.

 

이를 위해서는 다음과 같은 기능 구현이 필요합니다.

 

  • 로그인 폼에 '이메일 기억하기' 옵션을 추가
  • 로그인 시 '이메일 기억하기' 옵션을 선택했으면 로그인 후 쿠키에 이메일 저장(브라우저를 닫아도 삭제되지 않게 유효기간 길게 설정)
  • 이후 로그인 폼을 보여줄 때 이메일을 저장한 쿠키가 존재하면 입력 폼에 이메일을 보여줌

 

@CookieValue 애노테이션을 사용해 기능을 구현해 보겠습니다.

이 애노테이션은 적용된 메서드의 Cookie 타입 파라미터로 전달합니다.

 

LoginController.java

 

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

 

 

@CookieValue의 value 속성은 쿠키의 이름을 지정합니다. required의 기본 값은 true 이지만 이메일 기억하기를 선택하지 않을 수 있기 때문에 false로 지정해 줬습니다.

만약 true인데 예시처럼 "REMEMBER"라는 값이 존재하지 않는 경우에는 익셉션이 발생할 수 있습니다.

쿠키의 값이 존재한다면 커맨드 객체를 사용해서 폼을 출력하므로 입력 폼의 email 프로퍼티에 쿠키 값이 채워져서 출력됩니다.

그럼 쿠키의 값을 출력해보도록 하겠습니다.

 

 

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

 

 

로그인에 성공하면 이메일 기억하기를 선택했는지 여부에 따라 30일 동안 유지되는 쿠키를 생성하거나 바로 삭제되는 쿠키를 생성합니다.

이제 직접 실행해 보겠습니다.

 

 

 

실행 결과 로그인 성공 시 쿠키에 값이 잘 저장되었고, 다시 로그아웃 후 로그인 화면으로 돌아왔을 때 그 값을 잘 출력하는 것을 확인할 수 있습니다.

또한 오늘 날짜인 23.12.16일 기준 30일 후인 24.1.15일까지 기한을 가진 쿠키임을 확인할 수 있습니다.

 

 

 

 

출처: 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문

반응형