Spring

[SpringBoot] 쿠키, 세션을 이용한 로그인 처리

꾸준함. 2021. 7. 31. 17:57

개요

지난번 게시글에서는 쿠키, 세션, 그리고 쿠키와 세션을 이용한 로그인에 대한 개념을 정리해봤습니다.

(https://jaimemin.tistory.com/1885)

 

[SpringBoot] 쿠키, 세션 개념 정리

개요 이번 포스팅에서는 로그인 기능을 구현할 때 꼭 필요한 개념인 쿠키와 세션에 대해 정리해보겠습니다. 실제 로그인과 관련된 코드 및 설명은 다음 글에서 정리하겠습니다. 쿠키 서버에서

jaimemin.tistory.com

 

이번 게시글에서는 코드와 함께 지난번 게시글에 대한 부가 설명을 진행하겠습니다.

 

1. 쿠키를 이용한 로그인 처리

지난 게시글 복습

  • 서버는 로그인 요청이 성공할 경우 응답 헤더 내 Set-Cookie 필드에 사용자 정보 전달
  • 쿠키를 전달받은 클라이언트는 이후 모든 HTTP 요청 시 클라이언트 내 쿠키 저장소에서 쿠키를 꺼내 요청에 포함시킴
  • 로그아웃을 할 때 쿠키 또한 만료를 시켜야 함
    • 즉, 서버에서 해당 쿠키의 종료 날짜를 0으로 지정하면 해결 (expireCookie 메서드 참고)
  • 로그인 처리를 쿠키만 사용할 경우 각종 문제를 야기할 수 있는데 이는 개요에서 언급한 지난 게시글을 참고해주시면 감사하겠습니다.

 

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Validated @ModelAttribute("loginForm") LoginForm form
, BindingResult bindingResult
, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member member = loginService.login(form.getLoginId(), form.getPassword());
if (ObjectUtils.isEmpty(member)) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 성공 처리
// 쿠키에 시간 정보를 주지 않으면 세션 쿠키 (브라우저 종료 시 모두 종료)
Cookie cookie = new Cookie("memberId", String.valueOf(member.getId()));
response.addCookie(cookie);
return "redirect:/";
}
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
view raw .java hosted with ❤ by GitHub

 

 

2. 세션과 쿠키를 이용한 로그인 처리

지난 게시글 복습

  • 쿠키는 클라이언트에서 저장하는 반면, 세션은 서버에서 저장하고 있는 정보
  • 세션은 크게 세션 생성, 조회, 만료 처리의 기능을 가지고 있음
  • 쿠키만을 이용한 로그인 처리는 각종 보안 문제를 야기하므로 통상적으로 상용 서비스에서는 세션과 쿠키를 동시에 사용
    • 쿠키 값이 변조 가능하지만 세션 ID 같은 경우 UUID와 같이 불규칙적이고 유추하기 매우 어렵기 때문에 클라이언트가 의도적으로 쿠키 값을 변조하더라도 다른 고객의 정보가 나올 확률 매우 적음
    • 쿠키는 클라이언트 사이드에서 저장되기 때문에 노출되기 쉽지만 세션에서 생성한 sessionId의 경우 노출이 돼도 위험도가 적음
    • sessionId가 불규칙적이더라도 man in the middle attack을 통해 탈취한 sessionId를 그대로 사용할 경우 고객 정보 노출될 위험이 존재
      • 따라서, 세션의 만료시간을 짧게 유지하거나 ip가 갑자기 중국, 북한, 러시아 등과 같은 나라의 ip로 변경될 경우 세션 값을 만료시키는 방법으로 대처 가능
      • 아래 예제 코드에는 위 방법이 적용되어있지 않지만 실제 서비스를 운영할 때는 위 상황도 고려하여 코드를 작성하는 것을 권장

 

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private static final String LOGIN_MEMBER = "LOGIN_MEMBER";
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Validated @ModelAttribute("loginForm") LoginForm form
, BindingResult bindingResult
, HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member member = loginService.login(form.getLoginId(), form.getPassword());
if (member == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 성공 처리
// 세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
// 세션에 로그인 회원 정보 보관
session.setAttribute(LOGIN_MEMBER, member);
return "redirect:/";
}
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
}
view raw .java hosted with ❤ by GitHub

 

 

 

비고

ArgumentResolver를 이용하여 커스텀 애노테이션 정의

  • 컨트롤러에서 세션 내 attribute를 가지고 오기 위해서는 HttpServletRequest에서 세션을 불러온 뒤 getAttribute 메서드를 통해 값을 확인하거나 
  • @SessionAttribute 애노테이션을 컨트롤러 파라미터로 받아야 함
  • 페이지가 늘어날수록 로그인 여부를 확인하기 위해 위 코드를 반복해서 작성해야 하는데 ArgumentResovler를 통해 커스텀 애노테이션을 정의하면 코드량을 줄일 수 있음

 

1. 세션에서 로그인 정보를 받아오는 기존 방법

 

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private static final String SESSION_ID = "sessionId";
private final MemberService memberService;
// @GetMapping("/home")
public String homeLogin(HttpServletRequest request
, Model model) {
HttpSession session = request.getSession(false);
if (session == null) {
return "/login";
}
// 회원 정보 조회
String sessionId = (String) session.getAttribute(SESSION_ID);
Member member = memberService.getMember(sessionId);
// 세션에 회원 데이터가 없으면 home
if (member == null) {
return "home";
}
// 세션이 유지되면 로그인으로 이동
model.addAttribute("member", member);
return "loginHome";
}
@GetMapping("/home")
public String homeLoginV2(@SessionAttribute(name = SESSION_ID, required = false) String sessionId
, Model model) {
// 세션에 회원 데이터가 없으면 home
if (member == null) {
return "/login";
}
// 세션이 유지되면 로그인으로 이동
Member member = memberService.getMember(sessionId);
model.addAttribute("member", member);
return "loginHome";
}
}
view raw .java hosted with ❤ by GitHub

 

 

2. ArgumentResolver를 통해 커스텀 애노테이션을 정의한 코드

 

Login 커스텀 어노테이션

 

@Target(ElementType.PARAMETER) // 파라미터에서만 사용
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 애노테이션 정보 남아있음
public @interface Login {
}

 

LoginArgumentResovler.java (ArgumentResolver 정의)

 


@Slf4j
public class LoginArgumentResolver implements HandlerMethodArgumentResolver {
private static final String SESSION_ID = "sessionId";
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행"); // 캐시에 저장되어 supportsParameter는 한번만 실행
// @Login 커스텀 어노테이션 여부 확인
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
// sessionId는 문자열이므로 String 타입인지 확인
boolean hasStringType = String.class.isAssignableFrom(parameter.getParameterType());
// 두 개 다 만족해야지 해당 ArgumentResolver가 지원하는 타입
return hasLoginAnnotation && hasStringType;
}
@Override
public Object resolveArgument(MethodParameter parameter
, ModelAndViewContainer mavContainer
, NativeWebRequest webRequest
, WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
// 세션 내 해당 속성이 없을 경우 null
// 있으면 sessionId 반환
Object sessionId = session.getAttribute(SESSION_ID);
return sessionId;
}
}
view raw .java hosted with ❤ by GitHub

 

Configuration에 LoginArgumentResolver 등록


@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
view raw .java hosted with ❤ by GitHub

 

Controller에 @Login 애노테이션 적용


@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberService memberService;
@GetMapping("/home")
public String homeLoginV3ArgumentResolver(@Login String sessionid
, Model model) {
// 세션에 회원 데이터가 없으면 home
if (member == null) {
return "/login";
}
// 세션이 유지되면 로그인으로 이동
Member member = memberService.getMember(sessionId);
model.addAttribute("member", member);
return "loginHome";
}
}
view raw .java hosted with ❤ by GitHub

 

출처

HTTP 웹 기본지식 (김영한 강사님)

인프런 스프링 MVC 2편 (김영한 강사님

반응형