[Spring Web MVC] 로그인 처리 - Cookie / Session
패키지 구조를 먼저 설계하자.
도메인과 웹을 분리했는데, 도메인은 시스템이 구현하는 핵심 비즈니스 업무 영역을 말한다.
나중에 웹을 다른 기술로 변경한다고 해도 도메인은 그대로 유지할 수 있어야 한다. (도메인은 웹에 의존하지 않고, 웹은 도메인에 의존한다.)
도메인 : 비즈니스 로직 (서비스, 리포지토리, 모델, 엔티티)
웹 : HTTP 요청 처리하고 응답 (컨트롤러, 필터, 리스너)
public Member login(String loginId, String password){
// Optional<Member> findMemberOptional = memberRepository.findByLoginId(loginId);
// Member member = findMemberOptional.get();
// if(member.getPassword().equals(password)){
// return member;
// }else{
// return null;
// }
return memberRepository.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
회원을 조회하고 파라미터로 넘어온 비밀번호를 비교해 같으면 해당 회원을, 다르면 null을 반환한다.
이 때 Java8의 람다식을 이용하면 매우 편하다.
@Data
public class LoginForm {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
로그인을 검증할 때 사용하는 객체를 만들어 검증에 사용하자.
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form){
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디나 비번 확인하세요");
return "login/loginForm";
}
// success
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
로그인 컨트롤러에서 도메인에 있는 로그인 서비스를 호출해 로그인을 시도한다.
성공 시 홈 화면으로 이동하고, 실패 시 reject() 메서드로 ObjectError를 생성하고 로그인 화면으로 이동시킨다.
로그인 상태를 유지할 때는 쿠키를 사용한다.
로그인이 성공하면 서버에서 클라이언트로 응답을 보낼 때 쿠키를 담는다.
쿠키를 가지고 있는 클라이언트는 매번 요청할 때 마다 해당 쿠키를 보낸다.
@GetMapping("/")
public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model){
if(memberId == null){
return "home";
}
Member loginMember = memberRepository.findById(memberId);
if(loginMember == null){
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome"; // 로그인 사용자 전용 뷰
}
홈 화면에서도 로그인한 사용자의 정보를 보여주도록 한다.
쿠키 값으로 데이터베이스에 저장되는 로그인 id를 사용하고, 해당하는 회원이 있을 경우 모델에 정보를 담는다.
뷰를 렌더링 할 때는 타임리프를 사용해 모델에 있는 정보를 읽어서 렌더링한다.
@PostMapping("/logout")
public String logout(HttpServletResponse response){
Cookie cookie = new Cookie("memberId", null);
cookie.setMaxAge(0);
response.addCookie(cookie);
return "redirect:/";
}
로그아웃 시 쿠키를 삭제한다.
쿠키를 사용해서 로그인과 로그아웃을 성공적으로 구현한 것 같지만.. 클라이언트는 웹 브라우저에서 쿠키 값을 강제로 변경할 수 있다.
즉, 클라이언트가 다른 사용자의 정보를 무작위로 열람할 수 있게 되는데.. 쿠키에 개인정보나 결제 관련 정보가 있다면?
그런 웹 사이트는 아무도 사용하지 않을 것이다.
그러면 로그인과 로그아웃을 어떻게 처리해야 할까?
중요한 정보는 서버에 저장해야 하고, 클라이언트와 서버는 임의의 식별자로 연결해야 한다.
즉, 세션을 통해 로그인을 구현해야 한다.
클라이언트의 로그인 요청에 대해 해당하는 회원 정보가 있으면 세션을 생성한다.
여기서 세션 아이디는 UUID를 통해 추정이 불가능하게 설정한다.
클라이언트에게 전달되는 쿠키 값으로는 세션 아이디를 사용한다.
즉, 클라이언트와 서버는 쿠키로 연결되고 서버 내부의 세션에서 세션 아이디를 통해 회원을 조회할 수 있다.
복잡한 세션 아이디를 사용해 쿠키 값을 변조하는 경우을 막을 수 있고, 쿠키로 사용하는 세션 아이디가 털려도 세션 아이디로는 아무것도 할 수 없다.
또, 세션의 만료시간을 설정해 보안을 좀 더 강화할 수 있다.
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
//생성
public void createSession(Object value, HttpServletResponse response) {
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
}
//조회
public Object getSession(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie == null) {
return null;
}
return sessionStore.get(sessionCookie.getValue());
}
//만료
public void expire(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie != null) {
sessionStore.remove(sessionCookie.getValue());
}
}
//찾기
private Cookie findCookie(HttpServletRequest request, String cookieName) {
if (request.getCookies() == null) {
return null;
}
// getCookies로 받으면 쿠키배열이 반환됨
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
}
자바로 세션을 직접 구현한 예시이다.
서블릿도 세션을 지원하기 때문에 굳이 이렇게 만들어서 사용할 필요는 없지만, 예시를 통해 세션이 어떻게 동작하는지 이해하자.
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디나 비번 확인하세요");
return "login/loginForm";
}
// success
// 세션이 있으면 반환, 없으면 새로 생성
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
HttpServletRequest 객체를 파라미터로 전달받고, request.getSession() 메서드를 사용해 세션을 사용한다.
getSession(true) 가 기본값이고, 세션이 있으면 기존 세션을 사용하고 없으면 새로 만들어서 반환한다.
false를 넣어주면 세션이 있을 때 세션을 사용하는건 같지만 세션이 없는 경우 null을 반환한다.
세션을 저장할 때는 setAttribute 메서드를 사용한다.
@PostMapping
public String logout(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session != null){
session.invalidate();
}
return "redirect:/";
}
로그아웃은 세션을 가져오고 invalidate 메서드를 사용해 처리한다.
@GetMapping("/")
public String homeLoginSpring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model){
if(loginMember == null){
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome"; // 로그인 사용자 전용 뷰
}
스프링은 세션을 좀 더 편리하게 사용할 수 있도록 @SessionAttribute 애너테이션을 지원한다.
세션을 찾고 세션에 들어있는 데이터를 찾는 작업을 @SeesionAttribute 애너테이션이 처리해준다.
웹 브라우저가 쿠키를 지원하지 않을 때는 쿠키 대신 URL을 통해서 세션을 유지하는데, 이 때는 URL에 jsessionid를 계속해서 포함시켜 전달해야 한다.
서버 입장에서 클라이언트가 처음 로그인을 시도할 때는 해당 웹 브라우저가 쿠키를 지원하는 여부를 알 수 없기 때문에 쿠키 값과 URL에 jsessionid를 함께 넣어서 응답한다.
세션이 제공하는 정보들에 대해 알아보자.
sessionId : JSESSIONID 의 값으로, 말 그대로 세션의 아이디이다.
maxInactiveInterval : 세션의 유효기간
creationTime : 세션을 만든 시간
lastAccessedTime : 세션에 연결된 사용자의 최근 접속 시간
isNew : 새로 생성된 세션인지 여부
세션은 사용자가 로그아웃 할 때 삭제된다.
로그아웃 없이 웹 브라우저를 꺼버리면 세션의 삭제 없이 웹 브라우저가 종료되는데, HTTP는 비연결성이기 때문에 서버는 클라이언트가 웹 브라우저를 종료한지 모른다.
즉, 세션은 그대로 유지되는데.. 세션도 메모리이고 계속해서 메모리를 차지하고 있으면 서버에 부담이 될 수 있다.
그래서 세션을 무한정 보관하기보다는 일정 시간을 두고 삭제하는 방식으로 다뤄야한다.
즉, 클라이언트가 서버에 최근에 요청한 시간을 기준으로 30분 정도씩 세션을 유지해주면 클라이언트와 서버 모두 만족할 수 있다. (HttpSession 도 이 방식을 사용한다.)
세션에는 최소한의 데이터만 저장해야 함을 기억하자.
스프링 시큐리티를 사용하면 웹 보안과 세션, 쿠키를 쉽게 구현할 수 있다.
그럼에도 세션과 쿠키가 어떻게 동작하는지, 웹 보안이 기본적으로 어떻게 구성되어있는지 이해하는건 스프링 시큐리티를 사용할 때 매우 중요하니 기초 지식을 단단하게 다져 놓자.
'Spring > Spring Web MVC' 카테고리의 다른 글
[Spring Web MVC] 로그인 처리 - Filter / Interceptor (1) | 2023.07.11 |
---|---|
[Spring Web MVC] Multipart (1) | 2022.08.29 |
[Spring Web MVC] 스프링 타입 컨버터 (0) | 2022.08.28 |
[Spring Web MVC] API 예외 처리 (0) | 2022.08.26 |
[Spring Web MVC] 예외 처리와 오류 페이지 (0) | 2022.08.26 |
댓글
이 글 공유하기
다른 글
-
[Spring Web MVC] 로그인 처리 - Filter / Interceptor
[Spring Web MVC] 로그인 처리 - Filter / Interceptor
2023.07.11 -
[Spring Web MVC] Multipart
[Spring Web MVC] Multipart
2022.08.29 -
[Spring Web MVC] 스프링 타입 컨버터
[Spring Web MVC] 스프링 타입 컨버터
2022.08.28 -
[Spring Web MVC] API 예외 처리
[Spring Web MVC] API 예외 처리
2022.08.26