[Spring MVC 2편] 6. 로그인 - 쿠키, 세션

HJ·2023년 1월 18일
0

Spring MVC 2편

목록 보기
6/13

김영한 님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard


1. 패키지 구분

  • domain : Item, ItemRepository 등의 파일이 위치, 핵심 비지니스 영역

  • web : Controller, ItemSaveForm 등의 파일이 위치, 웹과 관련된 기술

  • web은 domain을 알고 있지만( 의존하지만 ) domain은 web을 모르도록( 참조하지 않도록 ) 설계해야한다




2. 로그인, 로그아웃 처리 - 쿠키

2-1. 쿠키

  • 로그인 상태를 유지하기 위해 매번 쿼리 파라미터를 보내는 것은 좋은 방법이 아님
    ➡️ 쿠키 사용

  • 로그인 성공 시 서버에서 HTTP 응답에 쿠키를 담아 브라우저에 전달하면, 브라우저는 앞으로 모든 요청에 쿠키 정보를 자동으로 포함시킨다

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

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


2-2. LoginController - 로그인 성공

// LoginController
public String login(HttpServletResponse response, @Validated @ModelAttribute LoginForm form, BindingResult bindingResult) {

    // 로그인 성공 시
    Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
    response.addCookie(idCookie);

    return "redirect:/";
}
  • 로그인을 성공하면 쿠키를 생성한다

  • 쿠키에 시간 정보를 주지 않았으니 세션 쿠키 ( 브라우저 종료 시 모두 종료 )

  • 쿠키 이름은 memberId / 값은 DB에 저장된 회원의 ID ( not 로그인 ID )

  • 생성한 쿠키를 HttpServletResponse에 담는다

    • 응답을 확인했을 때 서버가 보낸( 클라이언트가 받은 ) Response Header를 보면 Set-Cookie에 memberId=1 처럼 쿠키가 들어있다

    • 새로고침을 눌렀을 때 클라이언트가 보내는 Request Header를 보면 Cookie에 memberId=1처럼 쿠키가 들어있다


2-3. LoginController - 로그아웃

// LoginController
@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);
}
  • memberId라는 이름의 쿠키를 새로 만들고 MaxAge를 0으로 설정한다

  • 새로 만든 쿠키를 응답 메세지에 담아서 보낸다

  • 결과적으로 로그아웃을 누르면 쿠키가 사라지게 된다


2-4. HomeController

// HomeController
@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";
}
  • 클라이언트가 보낸 request에 담긴 쿠키를 이용한다

  • @CookieValue 를 사용해서 쿠키를 조회

    • required = false로 설정하여 쿠키가 없는 사용자도 접근할 수 있도록 한다

2-5. 쿠키 보안 문제

2-5-1. 문제점

  • 쿠키 값은 임의로 변경할 수 있다

    • 클라이언트가 웹 브라우저에서 강제로 쿠키 값을 변경하면 다른 사용자가 된다
  • 쿠키에 보관된 정보는 훔쳐갈 수 있다

    • 쿠키의 정보는 웹 브라우저에도 보관되고, 네트워크 요청마다 서버로 전달된다

    • 나의 로컬 PC 혹은 네트워크 전송 구간에서 다른 사람이 볼 수 있다

  • 해커가 쿠키를 훔쳐가면 평생 사용할 수 있다

    • 훔쳐간 쿠키로 악의적인 요청을 계속 시도할 수 있다

2-5-2. 대안

  • 쿠키에 memberId와 같이 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출한다

  • 서버에서 토큰과 사용자 id를 매핑해서 인식하도록 하고 서버에서 토큰을 관리한다

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

  • 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다

  • 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 된다




3. 로그인, 로그아웃 처리 - 세션

3-1. 세션

  • 쿠키의 보안 문제를 해결하려면 중요한 정보를 모두 서버에 저장하고, 서버와 클라이언트는 추정 불가능한 임의의 식별자 값으로 연결해야한다

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

  • 서버는 세션 저장소를 만들어 관리한다 ( key, value를 가진 형태 )


3-2. 세션을 이용한 로그인 과정

  1. 아이디, 비밀번호를 입력해서 로그인

  2. 서버로 입력한 정보가 넘어온다

  3. 회원 저장소에서 찾았을 때 회원도 있고 비밀번호도 일치

  4. 세션 저장소에 UUID를 통해 토큰( 랜덤 값 )을 생성해서 세션 저장소의 key( sessionId )로 사용하고 value는 회원을 저장

  5. 쿠키의 value에 sessionId를 넣어서 클라이언트에게 전달

  6. 클라이언트가 요청할 때 sessionId를 가진 쿠키가 항상 포함된다

  7. 서버는 쿠키 정보로 세션 저장소를 조회해서 로그인 시 보관한 세션 정보를 사용 ( 쿠키의 value가 세션 저장소의 key에 해당 )




4. 세션 기능 직접 구현

4-1. 기능 설명

  • 세션 생성

    • sessionId 생성( 임의의 추정 불가능한 랜덤 값 )

    • 세션 저장소에 sessionId와 보관할 값 저장

    • sessionId로 응답 쿠키를 생성해서 클라이언트에 전달

  • 세션 조회

    • 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회
  • 세션 만료

    • 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거

4-2. 세션 매니저 생성

@Component
public class SessionManager {

    public static final String SESSION_COOKIE_NAME = "mySessionId"; // 쿠키 이름
    private Map<String, Object> sessionStore = new ConcurrentHashMap<>();

    ...  
}
  • @Component를 통해 스프링 빈으로 등록되게 한다

  • 동시성 문제를 위해 세션 저장소를 ConcurrentHashMap으로 구현

  • 세션 생성, 세션 조회, 세션 만료에 해당하는 메서드를 정의


4-3. 세션 생성 메서드

public void createSession(Object value, HttpServletResponse response) {

    // sessionId 생성하고 값을 세션에 저장
    String sessionId = UUID.randomUUID().toString();
    sessionStore.put(sessionId, value);

    // 쿠키 생성
    Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
    response.addCookie(mySessionCookie);

}
  • 세션 저장소의 value에 저장할 Object를 파라미터로 받는다

  • String sessionId = UUID.randomUUID().toString(); : 절대 중복되지 않는 임의의 값 생성 ( 세션 저장소의 key로 사용 )

  • 생성된 sessionId( 랜덤 토큰 )를 쿠키의 value에 넣고, HttpServletResponse 객체에 쿠키를 넣는다


4-4. 세션 조회 메서드

public Object getSession(HttpServletRequest request) {

    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);

    if (sessionCookie == null) {
        return null;
    }

    return sessionStore.get(sessionCookie.getValue());
}
  • 클라이언트가 보낸 request에 있는 쿠키를 사용해서 조회

  • findCookie()

    • Cookie[] cookies = request.getCookies(); : HTTP 요청에 있는 모든 쿠키를 꺼낸다

    • 전달한 SESSION_COOKIE_NAME에 해당하는 쿠키가 있는지 찾아서 반환한다

  • sessionStore.get(sessionCookie.getValue()) : 쿠키의 value가 세션 저장소의 key이기 때문에 세션 저장소에서 해당하는 Object를 반환
  • 세션 만료의 경우, findCookie()로 쿠키를 찾고 null이 아니면 remove()를 통해 세션 저장소에서 제거

4-5. 세션 만료 메서드

public void expire(HttpServletRequest request) {

    Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);

    if (sessionCookie != null) {
        sessionStore.remove(sessionCookie.getValue());
    }
}

4-6. 세션 적용하기

4-6-1. LoginController - 로그인 성공

// LoginController
public String login(HttpServletResponse response, @Validated @ModelAttribute LoginForm form, BindingResult bindingResult) {

    // 로그인 성공 시
    sessionManager.createSession(loginMember, response);
    return "redirect:/";
}
  • 기존처럼 직접 쿠키를 만들지 않고 sessionManager를 통해 세션을 생성

    • 세션을 생성하는 메서드를 호출하면 내부에서 세션 저장소에 저장하고, 쿠키도 생성

    • 생성된 쿠키를 HttpServletResponse에 담아준다


4-6-2. LoginController - 로그아웃

@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
    sessionManager.expire(request);
    return "redirect:/";
}
  • 로그아웃 버튼을 누르면 세션이 제거되도록

4-6-3. HomeController

// HomeController
@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";
}
  • 사용자가 따라 다른 화면을 출력하는 부분에서 쿠키를 직접 활용하는 것이 아닌 sessionManager를 활용

    • sessionManager가 requeset에서 쿠키를 찾고 쿠키에서 세션 저장소의 key에 해당하는 값을 반환

    • sessionManager가 세션 저장소의 value에 담긴 Object를 반환




5. HttpSession

5-1. HttpSession

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

  • HttpSession이 위에서 만든 SessionManager와 같은 동작을 수행

  • 서블릿을 통해 HttpSession을 생성하면 JSESSIONID라는 이름의 쿠키를 생성, value는 역시 추정 불가능한 랜덤값


5-2. LoginController - 로그인 성공

// LoginController
public String login(HttpServletResponse response, @Validated @ModelAttribute LoginForm form, BindingResult bindingResult) {

    // 로그인 성공 시
    HttpSession session = request.getSession();
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

    return "redirect:/";
}
  • request.getSession()

    • 세션이 있으면 기존 세션을 반환

    • 세션이 없는 경우 () 안에 true(default)를 작성하면 새로운 세션을 생성해서 반환

    • fasle이면 null을 반환

  • session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember) : 세션에 데이터를 보관

  • getSession()은 request에서 얻어온 쿠키의 value에 저장된 랜덤세션id값으로 세선 저장소에서 랜덤세션id이 일치하는 세션을 찾는 것이고,

  • getSession()을 통해 찾아진 세션 내부에 setAttribute()로 key값을 SessionConst.LOGIN_MEMBER로 하고 value에 member에 대한 정보를 저장한다

  • 즉, 실제로 세션들을 보관하는 세션 저장소가 하나 있다 ( 랜덤세션id를 key로, Map을 value로 )

    • 이 랜덤세션id를 통해 특정 사용자만이 사용하는 Map 객체를 가지고 온다 ( 특정 세선을 가지고 온다 )

    • Map 객체가 SessionConst.LOGIN_MEMBER를 key로, loginMember를 value로 가지고 있다

    • 랜덤세션id는 tomcat이 생성한다


5-3. LoginController - 로그아웃

// LoginController
public String logoutV3(HttpServletRequest request) {

    HttpSession session = request.getSession(false);

    if (session != null) {
        session.invalidate();
    }

    return "redirect:/";
}
  • 세션을 가져올 때 세션이 없는 경우 만들지 않고 null을 반환하도록 false로 작성

5-4. HomeController

// HomeController
@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);

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

    // 세션이 유지되면 로그인으로 이동
    model.addAttribute("member", loginMember);
    return "loginHome";
}
  • session.getAttribute(SessionConst.LOGIN_MEMBER) : 찾아진 세션 내부에서 SessionConst.LOGIN_MEMBER를 key값으로 value를 찾는다

5-5. HomeController 개선 - @SessionAttribute

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

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

    // 세션이 유지되면 로그인으로 이동
    model.addAttribute("member", loginMember);
    return "loginHome";
}
  • 세션 저장소에서 세션을 찾고, 찾아진 세션에서 member를 가져오는 과정을 @SessionAttribute로 간단하게 처리 가능

  • @SessionAttribute는 세션을 생성하지 않는다


5-6. TrackingModes

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

  • 로그인을 처음 시도하면 위처럼 url에 jsessionid가 포함되어 있다

  • 이렇게 url에 포함시키는 방법은 웹 브라우저가 쿠키를 지원하지 않을 때 사용하고 매 요청마다 붙여주어야 한다

  • 타임리프 같은 템플릿 엔진을 통해 링크를 걸면 자동으로 url에 jsessionid를 포함시켜준다

  • 웹 브라우저가 쿠키를 지원해도 최초 1회는 url에 jsessionid를 포함시킨다

    • 처음에는 웹 브라우저가 쿠키를 지원하는지 지원하지 않는지 판단하지 못하기 때문에 쿠키도 전달하고 url에 포함시키는 것도 한다

    • 최초에도 수행되지 않게 하려면 application.properties에 아래 코드를 작성한다

    • server.servlet.session.tracking-modes=cookie




6. 세션 정보 조회

  • sessionId : 세션Id, jsessionid 의 값
  • maxInactiveInterval

    • 세션의 유효 시간 ( 초 )

    • 비활성화 시키는 최대 시간 간격

  • creationTime : 세션 생성일시
  • lastAccessedTime

    • 세션과 연결된 사용자가 최근에 서버에 접근한 시간

    • 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신

  • isNew : 새로 생성된 세션인지 ( true ), 이미 과거에 만들어진 세션인지 여부 ( false )



7. 세션 타임아웃 설정

7-1. 타임아웃 필요성

  • 사용자가 로그아웃 버튼을 눌러야 session.invalidate()가 호출되어 세션이 삭제되는데 대부분의 사용자는 그냥 웹 브라우저를 종료

  • 이런 경우, HTTP는 비연결성이므로 서버는 사용자가 웹 브라우저를 종료한 것인지 알 수 없어서 언제 삭제해야하는지 판단하기 어렵다

  • 세션은 메모리를 사용하기 때문에 적절하게 지워주어야 한다


7-2. 타임아웃 방법

  • 세션을 마지막 요청한 시간 기준으로 30분 정도를 유지해준다 ➜ 사용자가 계속 사용한다면 세션의 생존 시간이 계속 30분으로 늘어나게 된다

    • HttpSession이 사용하는 방식

    • 세션의 타임아웃 시간은 해당 세션과 관련된 jsessionid 를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화

    • 이렇게 초기화 되면 세션 타임아웃으로 설정한 시간동안 세션을 추가로 사용할 수 있다

    • session.getLastAccessedTime() : 최근 세션 접근 시간

    • LastAccessedTime 이후로 timeout 시간이 지나면, WAS가 내부에서 해당 세션을 제거

  • 글로벌 타임아웃 설정

    • application.properties에 아래처럼 설정

    • server.servlet.session.timeout=60 : 60초 ( 1분 )

    • 글로벌 타임아웃은 분 단위로 설정해야함 ( 60의 배수로 설정 )

  • 특정 세션의 타임아웃 설정

    • session.setMaxInactiveInterval(1800); : 1800초
profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글