본문 바로가기
Backend, Server/Spring MVC

[Spring MVC] 쿠키와 세션

by ggyongi 2022. 2. 4.
반응형

쿠키의 기본 개념은 생략하고,

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

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

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

 

쿠키 생성

- 다음과 같이 생성하면 된다. 생성한 쿠키는 HttpServletResponse에 담으면 된다.

- 생성자는 (쿠키 이름, 값) 형태다.

Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);

 

쿠키 조회

- @CookieValue로 조회 가능하다.
- 로그인하지 않은 사용자도 해당 url에 접근할 수 있게 하려면 required 설정을 꼭 false로 설정해야 한다. 

- 예시(쿠키 존재 여부를 통해 로그인 여부 확인)

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

 

쿠키 삭제

- 세션 쿠키의 경우 웹 브라우저가 종료될때 삭제되거나,

- 서버에서 해당 쿠키의 종료 날짜를 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);
  }

 

쿠키의 보안 문제

1. 쿠키 값이 임의로 변경될 수 있음

 - 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 됨

 - 웹브라우저의 개발자모드 -> Application -> 쿠키 변경으로 확인 가능

 

2. 쿠키 정보는 도난될 수 있음

 - 그래서 쿠키에 중요 정보를 넣지 않는 방법을 사용해야 함

 

3. 해커가 쿠키를 한번 훔치면 평생 사용 가능

 - 훔친 쿠키로 계속 악의적인 요청을 시도할 수 있다.

 

대안은?

- 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출하고, 서버에서 토큰과 사용자 id를 매핑해서 인식한다. 그리고 서버에서 토큰을 관리한다.

- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.

- 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게(예: 30분) 유지한다. 또는 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 된다.

 

=> 대안은 세션이다!

 

 

 

세션의 동작 방식

- 쿠키를 주고받을 때 세션id를 주고받는다. 이때 세션id는 추정 불가능해야 한다.

- 서버는 세션 저장소를 만들어, 세션id와 값을 보관한다.

- 이렇게 하면 쿠키에는 중요 정보를 담지 않을 수 있다. 오직 추정 불가능한 id만을 주고받게 된다.

 

문제 해결

- 쿠키 값 변조 가능 -> 예상 불가능한 복잡한 세션 id 사용

- 쿠키에 중요 정보가 들어갈 수 있다 -> 세션 id가 털려도 중요 정보를 알아낼 수 없다.

- 탈취한 쿠키 무기한 사용 -> 세션 만료 시간(보통 30분)을 설정하여 짧게 유지

 

 

세션의 생성, 조회, 만료 기능 직접 만들기

- 아래의 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());
        }
    }


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

 

 

서블릿 HTTP 세션

- 세션 관리 클래스를 그럼 매번 직접 만들어야 하나? NO

- 서블릿은 세션을 위해 HttpSession이라는 기능을 제공해줌

- 직접 만든 것보다 더 다양한 기능을, 더 쉽게 제공 => 결론은 이걸 쓰자..!

 

 

서블릿 HTTP 세션 사용법

<생성>

- request.getSession()를 사용하면 됨

public HttpSession getSession(boolean create);

- 옵션으로 create값을 설정 가능(default는 true)

true : 세션이 있으면 기존 세션 반환, 세션이 없으면 새 세션 생성하여 반환

false : 세션이 있으면 기존 세션 반환, 세션이 없으면 새 세션 생성하지않고 null 반환

 

<세션에 데이터 보관>

- session.setAttribute()를 사용하면 됨

session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

 

<세션 제거>

session.invalidate()

 

 

 

서블릿 HTTP 세션 - @SessionAttribute

스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute 을 지원한다.

이미 로그인 된 사용자를 찾을 때는 다음과 같이 사용하면 된다. 참고로 이 기능은 세션을 생성하지 않는다. @SessionAttribute(name = "loginMember", required = false) Member loginMember

 

- 활용 전

 @GetMapping("/")
 public String homeLoginV3(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";
 }

- 활용 후

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

        // 세션에 회원 데이터가 없으면 홈으로 이동
        if (loginMember == null) {
            return "home";
        }

        // 세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }

 

 

TrackingModes

로그인을 처음 시도하면 URL이 다음과 같이 jsessionid 를 포함하고 있는 것을 확인할 수 있다.

http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872

 

이것은 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다. 이 방법을 사용하려면 URL에 이 값을 계속 포함해서 전달해야 한다. 타임리프 같은 템플릿은 엔진을 통해서 링크를 걸면 jsessionid 를 URL에 자동으로 포함해준다. 서버 입장에서 웹 브라우저가 쿠키를 지원하는지 하지 않는지 최초에는 판단하지 못하므로, 쿠키 값도 전달하고, URL에 jsessionid 도 함께 전달한다.

 

URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면 다음 옵션을 넣어주면 된다. 이렇게 하면 URL에 jsessionid 가 노출되지 않는다.

//application.properties
server.servlet.session.tracking-modes=cookie

 

 

세션 타임아웃 설정

- 세션이 제공하는 다양한 정보들

@Slf4j
@RestController
public class SessionInfoController {

    @GetMapping("/session-info")
    public String sessionInfo(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return "세션이 없습니다.";
        }

        //세션 데이터 출력
        session.getAttributeNames().asIterator()
                .forEachRemaining(name -> log.info("session name ={}, value = {}", name, session.getAttribute(name)));

        log.info("sessionId ={}", session.getId());
        log.info("maxInactiveInterval ={}", session.getMaxInactiveInterval());
        log.info("creationTime ={}", new Date(session.getCreationTime()));
        log.info("lastAccessedTime ={}", new Date(session.getLastAccessedTime()));
        log.info("isNew ={}", session.isNew());

        return "세션 출력";
    }
}

sessionId : 세션Id, JSESSIONID 의 값이다. 예) 34B14F008AA3527C9F8ED620EFD7A4E1

maxInactiveInterval : 세션의 유효 시간, 예) 1800초, (30분)

creationTime : 세션 생성일시

lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.

isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부

 

 

<타임아웃 설정법>

- 로그아웃 시에 세션이 만료되도록 설정을 하면, 대부분의 사용자는 로그아웃을 하지 않고 그냥 웹 브라우저를 종료하기 때문에 서버 입장에서는 종료 여부를 알 수 없다. 그렇다고 세션을 무한정 보관하면 문제가 생길 수 있다. (해커가 탈취한 쿠키를 계속 사용, 또는 메모리 문제)

 

- 글로벌 설정

//application.properties
server.servlet.session.timeout=60 : 60초, 기본값은 1800(30분)

- 특정 세션에만 설정

session.setMaxInactiveInterval(1800); //1800초

 

 

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

 

비전공자 네카라 신입 취업 노하우

시행착오 끝에 얻어낸 취업 노하우가 모두 담긴 전자책!

kmong.com

댓글