spring

Session Cookies

noAb 2024. 5. 31. 22:55

웹 사이트에서 가장 흔한 기술이 로그인, 로그아웃일 것입니다. 한번 로그인을 하면 로그아웃을 하거나 시간이 지나지 않는 이상 사이트를 이용하는 동안 계속해서 사이트는 저의 존재를 구분하여 사용이 가능하게 해줍니다. 이때 사용되는 기술이 쿠키와 세션입니다.

흔히 간단하게 알고있는 지식으로는 쿠키는 클라이언트에 보관이 되고 세션은 서버에서 저장이 된다고 알고 있습니다.

쿠키만 사용하면 안될까?

기본적으로 정보를 저장하는데 있어서 클라이언트에만 의존하게 된다면 보안에 취약하다고 생각하게 됩니다.

F12키를 눌러서 네트워크에 보시면 현재 사용중인 쿠키의 정보가 그대로 노출이 됩니다. 정보가 그렇게 쉽게 노출이 되면 안되겟죠..

 

세션만 사용하면 안될까?

기본적으로 클라이언트에서 서버에 요청을 보낼때 매번 요청이 생성이 되는데 로그인이 된 상태를  유지하기 위해 매번 요청에 로그인에 관한 쿠키를 보관하고 매번 요청에 포함되어야합니다.

 

쿠키와 세션의 조합

클라이언트에서 로그인 정보를 입력해서 서버에 전송을 하면 랜덤으로 만들어진 sessionId를 key로 가진 session에 value로 로그인 정보가 저장이 되고 해당 key값을 value로 갖는 mySessionId라는 쿠키를 만들어 클라이언트에 보내지게 됩니다.

이렇게 되면 클라이언트는 로그인정보에 대한 정보는 전혀 알수 없게 됩니다.


세션 직접 만들기

기본적으로 스프링 부트에서는 세션을 쉽게 사용할 수 있는 제공을 제공하지만 조금더 제대로 이해를 위한 세션처리를 직접 만들어 보겠습니다.

세션에 대한 처리를 위한 파일생성

SessionManager

@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를 생성하고, 값을 세션에 저장
        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;
         }
         return Arrays.stream(request.getCookies())
                 .filter(cookie -> cookie.getName().equals(cookieName))
                 .findAny()
                 .orElse(null);
    } 
}

로직은 사실 간단합니다.

세션 생성 : Object객체로 로그인정보를 받고 랜덤UUID를 생성해여 값을 session에 저장을 합니다. 그리고 session에 저장한 key값을 value로 쿠키를 생성하여 응답을 하게 됩니다.
세션 조회 : 페이지 요청시 세션을 조회하기 위해 key값을 이용하여 값의 유무를 판단하여 value(로그인정보)를 전달합니다.
세션 만료 : 로그아웃을 누르게 되면 exprie메서드가 호출이 되면서 key값을 찾아 해당 값을 remove하게 됩니다.

 

처리할 매핑 컨트롤러

LoginController

private final SessionManager sessionManager;

@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult
    bindingResult, HttpServletResponse response) {
    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";
    }
    //로그인 성공 처리
    //세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관 
    sessionManager.createSession(loginMember, response);
    return "redirect:/";
}

@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
    sessionManager.expire(request);
    return "redirect:/";
}

간단하게 로직을 설명을 드리면 login으로 들어오게 되면 입력한 값에 대해 검증을 먼저 거친 후 에러가 잇다면 입력창으로 return을 하고, 입력 값들로 레파지토리에서 값을 찾은뒤 찾은 멤버가 없다면 에러메세지를 가지고 입력창으로 return을 하게 됩니다. 멤버가 잇다면 sessionManager에서 생성한 createSession메서드를 통해 찾은 멤버를 세션에 담고 홈화면으로 이동을 하게 됩니다.

마찬가지로 logout을 하게되면 sessionManager에서 멤버를 담은 key값을 찾은 뒤 remove하는 모습입니다.

 

HomeController

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

HttpSession 사용 1

서블릿에서 제공하는 HttpSession도 결국 저희가 직접 만든 SessionManger와 비슷한 동작방식을 합니다.

서블릿을 통해 HttpSession을 생성하면 JSESSIONID라는 이름의 쿠키가 생성이 되고 값은 추정이 불가능한 랜덤으로 들어가게됩니다.

 

public static final String LOGIN_MEMBER = "loginMember";

 

Login

@PostMapping("/login")
public String loginV3(@Validated @ModelAttribute LoginForm loginForm, BindingResult bindingResult, HttpServletRequest request){
    if (bindingResult.hasErrors()){
        return "/login/loginForm";
    }
    Member loginMember = loginService.login(loginForm.getLoginId(),loginForm.getPassword());
    if (loginMember == null) {
        bindingResult.reject("loginFail","일치하는 정보가 없습니다.");
        return "/login/loginForm";
    }

    //세션관리자를 통해 세션을 생성하고 회원데이터 보관
    HttpSession session = request.getSession();
    session.setAttribute(SessionConst.LONG_MEMBER,loginMember);
    return "redirect:/";
}

로그인 화면에 입력을 하게 되면 회원정보가 일치하는지 확인을 한 뒤 HttpSession을 사용하여 위에서 생성한 LOGIN_MEMBER를 key로 사용하여 가져온 회원정보를 세션에 저장한 뒤 /로 redirect하게 됩니다.

 

Home

@GetMapping("/")
public String loginHomeV3( Model model,HttpServletRequest request){
    HttpSession session = request.getSession(false);
    if (session==null){
        return "home";
    }
    Member member = (Member) session.getAttribute(SessionConst.LONG_MEMBER);
    if (member == null){
        return "home";
    }
    model.addAttribute("member",member);
    return "loginHome";
}

Home에 와서 세션을 생성하지 않고 기존의 세션을 가져온뒤 세션이 없다면 home으로 LOGIN_MEMBER세션에 회원정보가 없다면 home으로 만약 잇다면 세션에 담긴 회원정보를 담아서 loginHome으로 이동하는 코드입니다.

 

Logout

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

로그아웃을 누르게 되면 기존에 세션들을 가져온 뒤 세션이 잇다면 모두 삭제하여 home으로 이동하는 코드입니다.


위의 홈에서 회원세션을 찾는 코드를 더 간결하게 사용하기 위해 스프링에서 제공하는 @SessionAttribute를 사용하시면 됩니다.

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

     if (loginMember == null){
         return "home";
     }
     model.addAttribute("member",loginMember);
     return "loginHome";
 }

이처럼 해당 애노테이션을 사용하면 조금더 간결하게 home과 loginhome으로 구분이 가능합니다.