개요
지난번 게시글에서는 쿠키, 세션, 그리고 쿠키와 세션을 이용한 로그인에 대한 개념을 정리해봤습니다.
(https://jaimemin.tistory.com/1885)
[SpringBoot] 쿠키, 세션 개념 정리
개요 이번 포스팅에서는 로그인 기능을 구현할 때 꼭 필요한 개념인 쿠키와 세션에 대해 정리해보겠습니다. 실제 로그인과 관련된 코드 및 설명은 다음 글에서 정리하겠습니다. 쿠키 서버에서
jaimemin.tistory.com
이번 게시글에서는 코드와 함께 지난번 게시글에 대한 부가 설명을 진행하겠습니다.
1. 쿠키를 이용한 로그인 처리
지난 게시글 복습
- 서버는 로그인 요청이 성공할 경우 응답 헤더 내 Set-Cookie 필드에 사용자 정보 전달
- 쿠키를 전달받은 클라이언트는 이후 모든 HTTP 요청 시 클라이언트 내 쿠키 저장소에서 쿠키를 꺼내 요청에 포함시킴
- 로그아웃을 할 때 쿠키 또한 만료를 시켜야 함
- 즉, 서버에서 해당 쿠키의 종료 날짜를 0으로 지정하면 해결 (expireCookie 메서드 참고)
- 로그인 처리를 쿠키만 사용할 경우 각종 문제를 야기할 수 있는데 이는 개요에서 언급한 지난 게시글을 참고해주시면 감사하겠습니다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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); | |
} | |
} |

2. 세션과 쿠키를 이용한 로그인 처리
지난 게시글 복습
- 쿠키는 클라이언트에서 저장하는 반면, 세션은 서버에서 저장하고 있는 정보
- 세션은 크게 세션 생성, 조회, 만료 처리의 기능을 가지고 있음
- 쿠키만을 이용한 로그인 처리는 각종 보안 문제를 야기하므로 통상적으로 상용 서비스에서는 세션과 쿠키를 동시에 사용
- 쿠키 값이 변조 가능하지만 세션 ID 같은 경우 UUID와 같이 불규칙적이고 유추하기 매우 어렵기 때문에 클라이언트가 의도적으로 쿠키 값을 변조하더라도 다른 고객의 정보가 나올 확률 매우 적음
- 쿠키는 클라이언트 사이드에서 저장되기 때문에 노출되기 쉽지만 세션에서 생성한 sessionId의 경우 노출이 돼도 위험도가 적음
- sessionId가 불규칙적이더라도 man in the middle attack을 통해 탈취한 sessionId를 그대로 사용할 경우 고객 정보 노출될 위험이 존재
- 따라서, 세션의 만료시간을 짧게 유지하거나 ip가 갑자기 중국, 북한, 러시아 등과 같은 나라의 ip로 변경될 경우 세션 값을 만료시키는 방법으로 대처 가능
- 아래 예제 코드에는 위 방법이 적용되어있지 않지만 실제 서비스를 운영할 때는 위 상황도 고려하여 코드를 작성하는 것을 권장
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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:/"; | |
} | |
} |

비고
ArgumentResolver를 이용하여 커스텀 애노테이션 정의
- 컨트롤러에서 세션 내 attribute를 가지고 오기 위해서는 HttpServletRequest에서 세션을 불러온 뒤 getAttribute 메서드를 통해 값을 확인하거나
- @SessionAttribute 애노테이션을 컨트롤러 파라미터로 받아야 함
- 페이지가 늘어날수록 로그인 여부를 확인하기 위해 위 코드를 반복해서 작성해야 하는데 ArgumentResovler를 통해 커스텀 애노테이션을 정의하면 코드량을 줄일 수 있음
1. 세션에서 로그인 정보를 받아오는 기존 방법
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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"; | |
} | |
} |
2. ArgumentResolver를 통해 커스텀 애노테이션을 정의한 코드
Login 커스텀 어노테이션
@Target(ElementType.PARAMETER) // 파라미터에서만 사용
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 애노테이션 정보 남아있음
public @interface Login {
}
LoginArgumentResovler.java (ArgumentResolver 정의)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} | |
} |
Configuration에 LoginArgumentResolver 등록
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
public class WebConfig implements WebMvcConfigurer { | |
@Override | |
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | |
resolvers.add(new LoginMemberArgumentResolver()); | |
} | |
} |
Controller에 @Login 애노테이션 적용
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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"; | |
} | |
} |
출처
HTTP 웹 기본지식 (김영한 강사님)
인프런 스프링 MVC 2편 (김영한 강사님
반응형
'Spring' 카테고리의 다른 글
[SpringBoot] HTML 예외처리와 예외 페이지 (0) | 2021.08.03 |
---|---|
[SpringBoot] Filter, Interceptor 개념 정리 및 로그인 처리 (4) | 2021.08.01 |
[SpringBoot] 쿠키, 세션 개념 정리 (0) | 2021.07.29 |
[SpringBoot] Validation 간단 정리 - 2 (Bean Validation) (0) | 2021.07.25 |
[SpringBoot] Validation 간단 정리 - 1 (BindingResult, Validator) (6) | 2021.07.16 |