둘셋 개발!

[spring mvc 2편-웹 개발 활용 기술 - 5 (1) ] 로그인 처리 - 쿠키, 세션 본문

SPRING/MVC

[spring mvc 2편-웹 개발 활용 기술 - 5 (1) ] 로그인 처리 - 쿠키, 세션

23 2022. 2. 5. 02:24

 

우선 로그인을 하기 위해 로그인 로직을 짜야한다.

 

로그인 핵심 비지니스 로직을 LoginService에 담는다.

로그인 서비스

@Service
  @RequiredArgsConstructor
  public class LoginService {
      private final MemberRepository memberRepository;
      
	  //return null이면 로그인 실패
     
      public Member login(String loginId, String password) {
          return memberRepository.findByLoginId(loginId)
                  .filter(m -> m.getPassword().equals(password))
                  .orElse(null);
} }

 

로그인 컨트롤러는 LoginService를 호출해서

로그인에 성공하면 홈화면으로 이동하고

실패하면 bindingResult.reject()를 사용해서 글로벌 오류를 생성한다.

(bindingResult.reject()는 전 포스팅에 자세히 올라와 있다)

 

로그인 컨트롤러

@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(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult) {
        
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
	}
        
        Member loginMember = loginService.login(form.getLoginId(),form.getPassword());
        log.info("login? {}", loginMember);
        
        if (loginMember == null) {
	    bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }
	//로그인 성공 처리 TODO 
    
    	return "redirect:/";
        
	}
}

 


쿠키 이용하기

보통 로그인을 하고나면 나의 정보를 웹페이지에서 계속 유지하고 싶다.

이를 위해서 쿠키를 사용한다.

서버에서 로그인에 성공하면 http응답에 쿠키를 담아서 브라우저에 전달한다.

그러면 브라우저는 앞으로 해당 쿠키를 지속해서 보내주면서 서버에게 나의 정보를 계속해서 알려준다.

 

참고로 쿠키에는 영속 쿠키와 세션 쿠키가 있다.

- 영속쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지
- 세션쿠키 : 만료 날짜를 생략하면 브라우저 종료시 까지만 유지

지금은 브라우저 종료시까지만 유지하고 싶기때문에 세션쿠키를 사용할 것이다.

 


로그인 컨트롤러에 로그인 성공시 세션 쿠키를 생성하도록 해보자

 

로그인 컨트롤러

@GetMapping("/login")
    public String loginForm(@ModelAttribute LoginForm loginForm) {
        return "login/loginForm";
    }

@PostMapping("/login")
    public String login(@Valid @ModelAttribute LoginForm loginForm,BindingResult bindingResult, HttpServletResponse response) {
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
        log.info("login? {}", loginMember);

        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        //성공로직
        Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        response.addCookie(idCookie);
        return "redirect:/";
    }

 

쿠키에 이름(memberId)과 값(멤버의 id)을 넣어주고 response에 addCookie를 해서 브라우저 보낸다.

그러면 브라우저에서는 서버가 응답헤더로 보낸 쿠키를 받을 수 있다.

마지막 코드를 보면 성공하면 홈화면으로 리다이렉트 해준다.

그러면 홈컨트롤러에서는 쿠키를 읽어드려서 회원조회를 할 수 있다.

 

홈 컨트롤러

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

 

@CookieValue

@CookieValue 애노테이션을 통해서 쿠키를 전달 받을 수 있다.
name속성에는 쿠키이름을 넣으면 되고, required속성에 True를 넣으면 쿠키이름이 존재하지 않으면 예외를 터트린다.

 

로그아웃을 하고 싶을 때는 쿠키의 만료시간을 0으로 만들면 된다.

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

 


이렇게 쿠키를 만들면 보안에 매우 취약하다.

쿠키의 값에 멤버의 id는 1부터 1씩 증가하기 때문에, 아무나 브라우저에서 쿠키의 값을 변경하면 다른 사용자로 로그인 할 수 있다.

간단히 말해서 쉽게 쿠키의 값을 예측할 수 있어서 보안 문제가 생긴다는 것이다.

 

따라서 대안은

1. 사용자 별로 예측 불가능한 토큰을 쿠키에 노출하고, 서버에서 토큰과 멤버id를 매핑해서 사용자를 인식하게 한다.
2. 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예측 불가능 해야한다.
3. 만료시간을 짧게 해야, 해커가 토큰을 털어가도 사용할 수 없게 해야 한다.

 


세션 이용하기

앞서 쿠키에서 생긴 문제들은 세션에서 해결할 수 있다.

결국 중요한 정보들은 서버에서 관리를 해야한다는 것이다.

 

클라이언트와 서버는 임의의 예측불가능한 값으로 연결해야하는데

서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라 한다.

 


세션 사용 과정

1. 사용자가 로그인을 하면 서버에서는 회원이 맞는지 확인하고

세션 저장소에 sessionId에 임의의 식별자 값을 넣고 value에는 회원 객체를 넣어둔다.

 

2. 서버에서는 쿠키에 식별자 값을 넣어 브라우저에게 보낸다.

브라우저에서는 쿠키 저장소에 그 식별자 값을 넣고, 웹사이트를 이용할 때 마다 그 쿠키를 보내면서 '로그인 된 사용자'라고 서버에게 알려준다.

3. 그러면 서버는 식별자 값(쿠키 값)으로 세션 저장소를 조회해서, 로그인시 보관한 세션 정보를 사용한다.


Http 세션 사용하기

서블릿을 통해 HttpSession 을 생성하면 다음과 같은 쿠키를 생성한다.

쿠키 이름이 JSESSIONID 이고, 값은 추정 불가능한 랜덤 값이다.
Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05

 

 

로그인 컨트롤러

@PostMapping("/login")
    public String login(@Valid @ModelAttribute LoginForm loginForm,
                          BindingResult bindingResult,
                          HttpServletRequest request) {
        if (bindingResult.hasErrors()) {
            return "login/loginForm";
        }

        Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
        log.info("login? {}", loginMember);

        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        //성공로직

        //세션이 있으면 세션 반환, 없으면 신규 세션 생성
        HttpSession session = request.getSession();
        //세션에 로그인 회원 정보를 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER,loginMember);

        return "redirect:/";
    }

최초 로그인 시 서버에서 세션을 만들고 서블릿이 임의의 값을 생성해서 쿠키를 만든다.

직접 실행해보면 알겠지만 생성된 세션의 id를 찍어보면 쿠키의 value값과 같다. (한 브라우저는 하나의 세션을 가짐)

그래서 브라우저는 쿠키를 통해 세션에 저장된 정보를 알 수 있는 것이다.

 

-세션 생성과 조회

  • request.getSession(true) : 세션이 있으면 기존 세션 반환, 세션이 없으면 새로운 세션 생성 후 반환
  • request.getSession(false) : 세션이 있으면 기존 세션 반환, 세션이 없으면 null 반환

 

-세션에 로그인 회원 정보 보관

session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
request.setAttributed와 비슷한 방식으로, 하나의 세션에 여러 값을 저장할 수 있다.

(실무에서는 객체 전체보다는 (memberId정도) 간략한 정보만을 넣음)

 

로그아웃 컨트롤러

@PostMapping("/logout")
  public String logout(HttpServletRequest request) {

	HttpSession session = request.getSession(false); 
    
	if (session != null) {
          session.invalidate();
	}
    
	return "redirect:/"
    
  }

session.invalidate() 를 하면 세션을 무효화 한다.

 

홈 컨트롤러

@GetMapping("/")
    public String homeLogin(HttpServletRequest request, Model model){

        HttpSession session = request.getSession(false);
        if (session == null){
            return "home";
        }

        Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);

        if (loginMember == null){
            return "home";
        }

        model.addAttribute("member", loginMember);
        return "loginHome";
    }

 

@SessionAtrribute 사용

이미 로그인 된 사용자를 찾을 때 애노테이션으로 간편하게 찾을 수 있다.

 

홈 컨트롤러

@GetMapping("/")
    public String homeLogin(@SessionAttribute(value = SessionConst.LOGIN_MEMBER,required = false)
            Member loginMember, Model model){

        if (loginMember == null){
            return "home";
        }

        model.addAttribute("member", loginMember);
        return "loginHome";
    }

세션을 찾고, 세션에 있는 데이터를 일일히 코드로 치는 번거로운 과정을 스프링이 제공해준다.

 

정리

1. 최소 로그인 시 서버에서 회원의 정보가 들어간 세션을 생성함.
2. 서블릿이 쿠키를 생성해서 브라우저에게 전달  ex ) JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05
(쿠키의 값은 세션의 id와 동일)
3. 이미 로그인 된 사용자를 찾을 때 브라우저가 전달 한 쿠키 값으로 세션에 보관된 회원 객체를 찾는다. 이때 @SessionAtrribute를 사용해서 세션을 쉽게 찾음

세션이 제공하는 정보들

  • sessionId : 세션 id, JSESSIONID 값이다.
  • maxInactiveInterval : 세션의 유효 시간, 초단위
  • creationTime : 세션 생성일시
  • lastAccessedTime : 세션과 연결된 사용자가 최근데 서버에 접근한 시간
  • isNew : 새로 생성된 세션인지 아니면 과거에 만들어지고 나서 조회된 세션인지

세션 타임아웃 설정

세션은 사용자가 로그아웃을 직접 호출해서 삭제된다.

하지만 사용자가 로그아웃을 직접 호출할 수도 있고 아니면 그냥 브라우저를 종료하는 경우도 있다.

이때 서버는 언제 서버에서 세션 데이터를 삭제해야하는지 판단하기가 어렵다.

만약 사용자가 많을 경우 세션이 무한정 많아지기 때문에 다음과 같은 문제가 발생할 수 있다. 

 

1. 세션과 관련된 쿠키를 탈취 당했을 경우, 오랜 시간이 지나도 계속 쿠키를 악의적으로 요청하면 만료기간이 연장되어 보안에 위험
2. 세션은 기본적으로 메모리에 생성되기 때문에, 많은 세션이 삭제되지 않고 유지될 경우 메모리의 문제가 발생

 

그렇다면 세션의 종료 시점은 언제 하는 것이 좋을까?

세션 생성 시점으로부터 30분이 적당하다고 한다.

하지만 사용자가 계속 이용중인데, 30분 마다 로그인을 하면 번거롭다.

따라서 가장 좋은 대안은 사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도를 유지하는 것이다.

 

-스프링부트로 글로벌 세션 타임아웃 설정

application.properties

server.servlet.session.timeout=60

 

-특정 세션 단위로 세션 타임아웃 설정

session.setMaxInactiveInterval(1800)

사용자가 서버에 최근에 요청한 시간은 session.getLastAccessedTime()으로 알 수 있다.

따라서 이 시간 이후로 타임아웃 시간이 지나면, was가 내부에서 해당 세션을 제거한다.

 

 


실무에서 유의할 점!!

-세션에는 최소한의 데이터만 보관할 것

-세션의 시간을 너무 길게 가져가면 메모리 사용이 누족되서 적당한 시간을 선택해야함. 기본이 30분임.

 

(참고 : 인프런 김영한 강사님 - 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 )