ABAP DUMP ERROR 24시

[Spring MVC] 쿠키와 세션 (AllInOne) 본문

[WEB]Back-end/Spring MVC

[Spring MVC] 쿠키와 세션 (AllInOne)

이운형 2022. 3. 28. 14:23
반응형

# 인프런 김영한의 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 개인적으로 정리한 글입니다.

 

1. 직접 JAVA 코드로 Cookie 와 Session 구현해보기

2. HttpSession 을 통해 Spring의 기술을 통해 구현해보기.

 

<기술면접 준비 + 핵심 정리> 


Q. 쿠키 vs  세션

쿠키는 "웹브라우저에 정보를 저장"

세션은 "서버에 정보를 저장"

 

쿠키는 "로그아웃과 무관하게 쿠키 보관소에 저장"

세션은 "로그아웃시 세션정보 삭제"

 

 

Q. 그럼 쿠키 왜써? 세션쓰지?

 

세션은 "서버"에 정보를 저장하는것이기 때문에 비용이 많이들고,

보관한 데이터 용량 * 사용자 수로 세션의 메모리 사용량이 급격하게 늘어나서 장애로 이어질 수 있다.

 

또한 서버와 웹브라우저는 결국 쿠키로 통신하기 때문에 쿠키를 안쓸수는 없다.

 

중요 정보는 서버 세션에!  중요하지 않는 정보는 쿠키에 보관!

쿠키를 key 세션을 좌물쇠잠긴 창고라고 생각하자.

 

세션의 시간을 너무 길게 가져가면 메모리 사용이 계속 누적 될 수 있으므로

적당한 시간을 선택하는 것이 필요하다.

 

Q. 도메인의 정의

도메인 = 화면, UI, 기술 인프라 등등의 영역은 제외한 시스템이 구현해야 하는 핵심 비즈니스 업무 영역

즉 ,web을 다른 기술로 바꾸어도 도메인은 그대로 유지할수 있어야한다.

 

Q. 쿠키의 정의

Http의 비연결성과 비상태성의 특성을 보완하고자,

서버가 클라이언트를 식별할수 있도록 생성되는 정보를 담은 임시 파일

 

Q. 쿠키의 특징

<Key , value> 로 구성되며 String 형태를 갖는다.

웹 브라우저를 사용하고 있는 컴퓨터에 저장이 된다.

 

Q. 쿠키의 종류

영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지

세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지

 

Q.쿠키의 보안문제

1. 쿠키값은 임의로 변경할수 있다,

2. 쿠키에 보관된 정보는 훔쳐갈수 있다.

3.해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.

 

Q.쿠키의 보안문제 해결방안

1. 쿠키에 중요한 값을 노출하지 않고사용자별로 예측 불가능한 임의의 토큰값을 노출(UUID 사용)

2. 서버에서 토큰의 사용자 ID를 매핑해서 인식후 서버에서 토큰을 관리한다.

 

Q. 세션의 정의

쿠키의 보안문제를 해결하고, 서버에서 웹 브라우저와의 연결을 지속시키는 역할.

 

Q.세션의 동작 방식

1.웹 브라우저로부터 loginId와 password를 정보를 받아서 서버에서 해당 사용자가 맞는지 확인한다.

2. 생성된 세션 ID와 세션에 보관할 value를 서버의 세션 보관소에 보관한다.

3. 서버에서 생성된 세션 ID를 웹브라우저에게 쿠키로 전달한다.

 

이때 서버는 클라이언트에 mySession이라는 이름으로 세션 ID만 쿠키에 담아 전달!

클라이언트는 쿠키 저장소에  mysessionID만 저장.

 

이때 클라이언트는 서버에게 세션ID만 쿠키로 저장받고 나머지 중요 정보들은 서버가 관리한다.

 

 

<코드 살펴보기>


1. 직접 JAVA 코드로 Cookie 와 Session 구현해보기

 

목표 1

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

로그인에 실패하면 "아이디 또는 비밀번호가 맞지 않습니다."라는 경고와 함께 로그인 폼이 나타나게 설계

 

<MemberRepository>

public Optional<Member> findByLoginId(String loginId){
    return findAll().stream()
            .filter(m -> m.getLoginId().equals(loginId))
            .findFirst();
}

<MemberService>

private final MemberRepository memberRepository;

public Member login(String loginId, String password){
    return memberRepository.findByLoginId(loginId)
            .filter(m -> m.getPassword().equals(password))
            .orElse(null);
}

<LoginController>

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {

    //첫번째
    
    private final LoginService loginService;

    @GetMapping("/login")
    public String loginForm(@ModelAttribute("loginForm") LoginForm loginForm){
        return "login/loginForm";
    }
    
    
    //@Valid는 loginForm 의 @NotNull과 같은 한정 어노테이션을 사용하겠다라는 의미!
    @PostMapping("/login")
    public String login(@Valid @ModelAttribute("loginForm") LoginForm loginForm, BindingResult bindingResult){
        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";
        }

        // 로그인 추가 처리(세션 쿠키 사용 예정)
         return "redirect:/";
    }

 

LoginController는 로그인 서비스를 호출해서 로그인에 성공하면 홈 화면으로 이동하고,

로그인에 실패하면 bindingResult.reject() 를 사용해서 글로벌 오류( ObjectError )를 생성

 

 

목표1 한계

로그인의 상태를 유지하면서, 로그인에 성공한 사용자는 홈 화면에 접근시 고객의 이름을 보여주려면 어떻게 해야할까?

 

목표2

쿠키를 사용해 보자.

 

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

그러면 브라우저는 앞으로 해당 쿠키를 지속해서 보내준다.

 

<종류>

영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지

세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지 

 

//version 1 에 HttpServletResponse , response.addCookie가 추가되었다.
@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginForm") 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:/";
}

 

목표1 과 목표2의 변경점

1. public String login에 HttpServletResponse 가 추가되었다. 

@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginForm") LoginForm loginForm, BindingResult bindingResult, HttpServletResponse response){

2.  로그인 성공이 되면 Cookie에 memberId에 loginMember에 해당하는 Id 내용들을 저장해서 브라우저에게 보내준다.

// 로그인 성공 처리
//쿠키에 시간 정보를 주지 않으면 세션 쿠키 종료
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);

 

 

3. @CookieValue를 추가해서 memberId에 해당하는 쿠키를 가져오는 설정.

required=false 를 통해 로그인 하지 않아도 접근 가능하게 설정한다.

 

<HomeController>

//required = false를 통해 로그인하지 않는 사용자도 홈에 접근할수 있게 해준다.
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";
}

 

4. logout 기능 추가

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

 

목표3 

Q.쿠키의 보안문제

1. 쿠키값은 임의로 변경할수 있다,

2. 쿠키에 보관된 정보는 훔쳐갈수 있다.

3.해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.

 

Q.쿠키의 보안문제 해결방안

1. 쿠키에 중요한 값을 노출하지 않고사용자별로 예측 불가능한 임의의 토큰값을 노출(UUID 사용)

2. 서버에서 토큰의 사용자 ID를 매핑해서 인식후 서버에서 토큰을 관리한다.

3. 해커가 토큰을 털어가도 서버에서 토큰의 만료시간을 짧게 유지한다.

 

즉, 중요한 정보는 서버에 저장해두도록 하자.

 

Q.세션의 동작 방식.

1. 웹 브라우저로부터 loginId와 password를 정보를 받아서 서버에서 해당 사용자가 맞는지 확인한다.

2. 생성된 세션 ID와 세션에 보관할 value를 서버의 세션 보관소에 보관한다.

3. 서버에서 생성된 세션 ID를 웹브라우저에게 쿠키로 전달한다.

 

이때 서버는 클라이언트에 mySession이라는 이름으로 세션 ID만 쿠키에 담아 전달!

클라이언트는 쿠키 저장소에  mysessionID만 저장.

 

이때 클라이언트는 서버에게 세션ID만 쿠키로 저장받고 나머지 중요 정보들은 서버가 관리한다.

 

 

 

JAVA로 직접 세션을 구현해 보자.

@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){

        //서버는 세션 id를 생성하는데 uuid를 사용해서 만든다.
        String sessionId = UUID.randomUUID().toString();
        //서버는 sessionStore에 uuid 세션 id와 실제값(value)을 넣는다
        sessionStore.put(sessionId, value);

        //서버는 웹브라우저 클라이언트에게 보낼 MysessionCookie를 만든다.
        //서버는 mySessionCookie에 uuid세션id와 그 값을 key로 사용할수 있는 세션저장소 index값을 넣는다.
        Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);

        //서버는 쿠키저장소에게 uuid세션id와 세션저장소값을 보낸다.
        response.addCookie(mySessionCookie);
    }

    /**
     * Session 조회
     */

    public Object getSession(HttpServletRequest request){
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME); // 세션 쿠키 이름과
        if(sessionCookie == null){
            return null;
        }
        return sessionStore.get(sessionCookie.getValue());
    }

    /**
     * 세션 만료
     */
    //여기의 request에는
    // client에 저장되있던 sessionCookie가
    // 서버로 날라와 key값을 매칭해보고 삭제할지 결정한다.
    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;
        }

        //request.getcookies()를  stream을 사용하면 배열로 들어온다.
        return Arrays.stream(request.getCookies())
                .filter(cookie -> cookie.getName().equals(cookieName))
                .findAny()
                .orElse(null);

    }

}

 

목표2 와 목표3의 변경점

1. 쿠키를 직접 생성해서 주입 하는 방식에서 세션의 기능을 활용한 방식으로의 변화

 

<목표 2의 코드> - 변경전

//        // 로그인 성공 처리
//        //쿠키에 시간 정보를 주지 않으면 세션 쿠키 종료
//        Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
//        response.addCookie(idCookie);

<목표 3의 코드> - 변경후

sessionManager.createSession(loginMember, response);
return "redirect:/";

 

과거 new Cookie("memberId", String.valueOf(loginMember.getId())); 를 통해

직접 주입했다면

목표 3은 sessionManager의 createSession 메소드를 통해

loginMember 와 response를 파라미터로 넘겨주어

세션을 생성하도록 한다.

 

로그인 성공시 세션에 loginMember를 저장해두고 쿠키를 발행한다.

 

<아래 그림 참조>

public void createSession(Object value, HttpServletResponse response){

    //서버는 세션 id를 생성하는데 uuid를 사용해서 만든다.
    String sessionId = UUID.randomUUID().toString();
    //서버는 sessionStore에 uuid 세션 id와 실제값(value)을 넣는다
    sessionStore.put(sessionId, value);

    //서버는 웹브라우저 클라이언트에게 보낼 MysessionCookie를 만든다.
    //서버는 mySessionCookie에 uuid세션id와 그 값을 key로 사용할수 있는 세션저장소 index값을 넣는다.
    Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);

    //서버는 쿠키저장소에게 uuid세션id와 세션저장소값을 보낸다.
    response.addCookie(mySessionCookie);

 

2. HomeController 의 주입 방식의 변화

 

<과거>

@CookieValue(name = "memberId")를 통한 쿠키를 찾고, required =false를 통해 쿠키가 없어도 들어갈수 있게 설계

 

<현재>

Member member = (Member) sessionManager.getSession(request); 코드를 통해

Member를 캐스팅 받고 HttpServletRequest를 통해 받은 request 값을 sessionMamager를 통해 세션을 추출해 낸다.

해당 유저에 대한 세션이 존재하지 않으면 home.html을

해당 유저에 대한 세션이 존재하면 loginHome.html를 나오도록 설계가 되었다.

 

<과거 코드>

//    @GetMapping("/")
    //required = false를 통해 로그인하지 않는 사용자도 홈에 접근할수 있게 해준다.
    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";
    }

<현재 코드>

  @GetMapping("/")
    public String homeLoginV2(HttpServletRequest request, Model model){
        Member member = (Member)sessionManager.getSession(request);
        if(member == null){
            return "home";
        }

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

 

2. HttpSession 을 통해 Spring의 기술을 통해 구현해보기.

 

private final LoginService loginService;

@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute("loginForm") 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:/";

목표3 와 목표4의 변경점

1.목표3은 SessionManager를 통해 직접 세션의 기능을 사용했으나, 목표4는 Servlet의 HttpSession의 기능을 사용하였음.

 

<직접 만든 createSession 메소드 기능을>

public void createSession(Object value, HttpServletResponse response){

    //서버는 세션 id를 생성하는데 uuid를 사용해서 만든다.
    String sessionId = UUID.randomUUID().toString();
    //서버는 sessionStore에 uuid 세션 id와 실제값(value)을 넣는다
    sessionStore.put(sessionId, value);

    //서버는 웹브라우저 클라이언트에게 보낼 MysessionCookie를 만든다.
    //서버는 mySessionCookie에 uuid세션id와 그 값을 key로 사용할수 있는 세션저장소 index값을 넣는다.
    Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);

    //서버는 쿠키저장소에게 uuid세션id와 세션저장소값을 보낸다.
    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());
}

 

 

<HttpSession을 이용하면 단 2줄만에 끝낼수 있다.>

//로그인 성공처리
//세션 존재시 해당 세션을 반환
HttpSession session = request.getSession();

//세션에 로그인 회원의 정보를 보관한다.
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

* HttpSession session = request.getSession(); 코드의 비밀

 

 

request.getSession(true)

세션이 있으면 기존 세션을 반환한다.

세션이 없으면 새로운 세션을 생성해서 반환한다.

 

request.getSession(false)

세션이 있으면 기존 세션을 반환한다.

세션이 없으면 새로운 세션을 생성하지 않는다. null 을 반환한다.

 

 

2. logout 기능의 변화

과거 Cookie에 MaxAge를 줘서 쿠키의 시간을 0으로 만들어 쿠키를 제거했다면,

지금은 session.invalidate()기능으로 세션을 제거한다.

//    @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);
//    }
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request){
    
    //세션을 삭제한다.
    HttpSession session = request.getSession(false);
    if (session != null){
        session.invalidate();
    }
    return "redirect:/";
}

 

3. HomeController 의 변화

과거에는 직접 만든 sessionManager을 통해 Member 확장자로 받아서 캐스팅을 진행했다면,

현제에는 Httpsession을 통해 세션의 존재 유무 판단후, 

session.getAttribute를 통해 로그인 시점에 보관한 회원 객체를 찾아 기능을 수행한다.

 

더쉽게 Spring의 @SessionAttribute 기능을 사용하면 더 간결하게 줄일수 있다.

//    @GetMapping("/")
    public String homeLoginV2(HttpServletRequest request, Model model){
        Member member = (Member)sessionManager.getSession(request);
        if(member == null){
            return "home";
        }

        model.addAttribute("member" ,member);
        return "loginHome";
/  @GetMapping("/")
  public String homeLoginV3(HttpServletRequest request, Model model){

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

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

      // 세션에 회원 데이터가 없으면 home으로
      if(loginMember == null){
          return "home";
      }
  //세션이 유지되면 로그인으로 이동
      model.addAttribute("member", loginMember);
      return "loginHome";
  }

 

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

        /**
         *
         * 다음 부분을 @SessionAttribute가  처리해준다.
         *
         *         HttpSession session = request.getSession(false);
         *         if (session == null){
         *             return null;
         *         }
         *
         *         Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
         *
         */

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

        model.addAttribute("member", loginMember);
        return "loginHome";
    }
반응형
Comments