로그인 처리 - 쿠키, 세션

YH·2023년 4월 24일
0

✅ 로그인 처리 - 쿠키

✔️ 로그인 및 쿠키 생성

//로그인 성공 시, 쿠키를 생성하고 저장한다.
@PostMapping("/login")
    public String login(@Validated @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";
        }

        //쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료 시 모두 종료)
        Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        response.addCookie(idCookie);

        //로그인 성공 처리
        return "redirect:/";
    }
  • 로그인 성공 후, 쿠키를 생성하고 HttpServletResponse에 쿠키를 담는다.

✔️ 기본 홈 화면 및 로그인 한 사용자에 대한 로그인 홈 화면 처리

@GetMapping("/")
    public String homeLogin(@CookieValue(name = "memberId", required = false) Long memberId, Model model) {
        //로그인 쿠키가 없으면 home으로 보냄
        if (memberId == null) {
            return "home";
        }

        //로그인 쿠키가 있어도 회원이 아니면 home으로 보냄
        Member loginMebmer = memberRepository.findById(memberId);
        if (loginMebmer == null) {
            return "home";
        }

        //로그인 쿠키가 있는 사용자는 로그인 사용자 전용 홈 화면인 loginHome으로 보낸다.
        //추가로 홈 화면에서 회원 관련 정보(member)도 출력해야 하므로 모델에 담아서 전달한다.
        model.addAttribute("member", loginMebmer);
        return "loginHome";
    }
  • home 접속 시, 해당 사용자의 로그인 쿠키과 사용자 정보가 있으면 쿠키를 통해 로그인 후 화면으로 이동한다.
  • 이동 시 사용자 정보에 대한 데이터(member)도 model에 담아 같이 전달한다.

✔️ 로그아웃 처리

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

✔️ 쿠키의 보안 문제

  • 쿠키 값은 임의로 변경할 수 있다.
    • 클라이언트가 쿠키를 강제로 변경하면 값이 변경된다.
  • 쿠키에 보관된 정보는 훔쳐갈 수 있다.
    • 쿠키에 개인 정보나 예민한 정보들이 들어 있다면, 이 정보는 웹 브라우저에도 보관되고, 네크워크 요청마다 계속 클라이언트에서 서버로 전달된다.
    • 그러면 이 쿠키 정보가 클라이언트 PC에서 또는 서버로 네트워크 전송이 이루어질 때 쿠키를 훔쳐갈 수 있다.
  • 한번 훔쳐간 쿠키는 계속해서 이용할 수 있다.

✔️ 대안

  • 쿠키에 중요한 값을 노출하지 않고, 예측 불가능한 임의의 토큰을 노출하고, 서버에서 토큰과 사용자 id를 매핑해서 인식한다. 서버에서 해당 토큰을 관리한다.
  • 토큰은 값을 찾을 수 없도록 예상 불가능해야 한다.
  • 토큰을 오랜 시간 사용할 수 없도록 만료시간을 짧게 유지한다.

✅ 로그인 처리 - 세션 동작 방식

✔️ 목표

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

✔️ 세션 동작 방식

  • 세션 ID를 생성하는데, 해당 ID는 UUID등 을 사용하여 추정이 불가능 하도록 한다.
  • 생성된 세션ID와 세션에 보관할 값을 서버의 세션 저장소에 보관한다.

✔️ 정리

  • 세션을 통해 쿠키에서 발생한 문제점들을 보완할 수 있다.
    • 쿠키 값을 변조 가능 → 예상 불가능한 복잡 세션 ID를 사용하므로 변조가 매우 어렵다.
    • 쿠키 정보를 해킹당할 수 있다. → 쿠키 정보를 해킹당해도 안에는 무의미한 세션 ID 값만 있으므로 중요 정보를 보호할 수 있다.
    • 쿠키 탈취 후 사용 → 탈취하여도 시간이 지나면 사용할 수 없도록 세션의 만료시간을 짧게 유지한다.

✅ 로그인 처리 - 세션 구현

✔️ 세션 구현 시, 아래의 중요 3가지 기능을 구현한다.

  • 세션 생성
    • 세션 ID 생성 (임의의 추정 불가한 랜덤 값)
    • 세션 저장소에 세션 ID와 보관할 값 저장
    • 세션 ID로 응답 쿠키를 생성해서 클라이언트에 전달
  • 세션 조회
    • 클라이언트가 요청한 세션 ID 쿠키 값으로, 세션 저장소에 보관된 값 조회
  • 세션 만료
    • 클라이언트가 요청한 세션 ID 쿠키 값으로, 세션 저장소에 보관한 세션 ID와 값 제거
@Component
public class SessionManager {

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

    /**
     * 세션 생성
     * @param value {@link Object}
     * @param response {@link HttpServletResponse}
     */
    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);
    }

    /**
     * 세션 조회
     * @param request {@link HttpServletRequest}
     * @return Object {@link Object}
     */
    public Object getSession(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie == null) {
            return null;
        }

        return sessionStore.get(sessionCookie.getValue());
    }

    /**
     * 세션 만료
     * @param request {@link HttpServletRequest}
     */
    public void expire(HttpServletRequest request) {
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie != null) {
            sessionStore.remove(sessionCookie.getValue());
        }
    }

    /**
     * 쿠키 조회
     * @param request {@link HttpServletRequest}
     * @param cookieName {@link String}
     * @return
     */
    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);
    }
}

✔️ 구현한 세션 테스트

public class SessionManagerTest {
    SessionManager sessionManager = new SessionManager();

    @Test
    void sessionTest() {
        //세션 생성
        MockHttpServletResponse response = new MockHttpServletResponse();
        Member member = new Member();
        sessionManager.createSession(member, response);

        //요청에 응답 쿠키 저장
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setCookies(response.getCookies());

        //세션 조회
        Object result = sessionManager.getSession(request);
        assertThat(result).isEqualTo(member);

        //세션 만료
        sessionManager.expire(request);
        Object expired = sessionManager.getSession(request);
        assertThat(expired).isNull();
    }
}

✅ 로그인 처리 - 서블릿 HTTP 세션 1

✔️ 서블릿에서는 세션을 위해 HttpSession 이라는 기능을 제공하는데, 위에서 직접 만든 SessionManager와 같은 방식으로 동작한다.
✔️ 서블릿을 통해 HttpSession을 생성하면 아래와 같은 쿠키를 생성한다.

  • 쿠키 이름 : JSESSIONID
  • 값 : 추정 불가능한 랜덤 값

✔️ HttpSession 사용

@PostMapping("/login")
    public String loginV3(@Validated @ModelAttribute LoginForm form, BindingResult bindingResult,
                          HttpServletRequest request) {

        ...

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

        //로그인 성공 처리
        return "redirect:/";
    }
  • 세션을 생성하려면 request.getSession(true)를 사용한다.
    • request.getSession(true)
      • 세션이 있으면 기존 세션을 반환한다.
      • 세션이 없으면 새로운 세션을 생성해서 반환한다.
    • request.getSession(false)
      • 세션이 있으면 기존 세션을 반환한다.
      • 세션이 없으면 새로운 세션을 생성하지 않는다. null을 반환
    • request.getSession()은 request.getSession(true)와 동일
  • 세션에 데이터를 보관하는 방법은 session.setAttribute(세션 명, 값), 하나의 세션에 여러 값을 저장할 수 있다.

✔️ 세션 제거

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

        return "redirect:/";
    }

✔️ 세션을 통한 Home 처리

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

        //세션이 없으면 home
        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); : 로그인 시점에 세션에 보관된 회원 객체를 찾는다.

✅ 로그인 처리 - 서블릿 HTTP 세션 2

✔️ 서블릿에서는 세션을 더 편리하게 사용하도록 @SessionAttribute 기능을 지원한다.

//HomeController
@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";
    }
  • @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginmember 와 같이 사용하며 해당 기능은 세션을 새로 생성하지 않는다.

✔️ TrackingModes

  • 로그인을 처음 시도하면 아래와 같이 URL에 jsessionId가 포함된다.
    http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
  • 이 것은 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다.
  • 타임리프 같은 템플릿은 엔진을 통해서 링크를 걸면 jsessionid를 URL에 자동으로 포함해준다. 서버 입장에서 웹 브라우저가 쿠키를 지원하는지 안하는지 최초에는 모르기 때문에 쿠키 값과 더불어 URL에 jsessionid도 같이 전달하는 것이다.
  • URL 전달 방식을 끄고 쿠키를 통해서만 세션을 유지하려면 아래 옵션을 넣어주면 URL에 jsessionid가 노출되지 않는다.
    • application.yaml
    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 값이다.
  • maxInactiveInterval : 세션의 유효 시간, 예) 1800초
  • creationTiem : 세션 생성일시
  • lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId(JSESSIONID)를 요청한 경우 갱신
  • isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트 서버로 sessionId(JSESSIONID)를 요청해서 조회된 세션인지 여부

✔️ 세션 타임아웃 설정

  • 세션은 사용자가 로그아웃을 하면 session.invalidate()가 호출되는 경우에 삭제된다.
  • 그런데 대부분 사용자는 로그아웃을 선택하지 않고, 그냥 웹 브라우저를 종료한다. HTTP는 비연결성(stateless)이므로 사용자가 웹브라우저를 종료했는지 아닌지 알 수 없다.
  • 따라서 세션의 종료 시점을 정해서 세션을 삭제해주어야 한다.

✔️ 세션의 종료 시점

  • 세션의 종료 시점은 사용자가 서버에 최근에 요청한 시간을 기준으로 일정 기간을 유지하도록 하고, 마지막 요청이 정해진 시간을 지나면 삭제하도록 하는 방식이 효율적이다.
  • HttpSession 기능도 위 방식을 사용한다.

✔️ 세션 타임아웃 설정

  1. 스프링 부트 글로벌 설정
  • application.yaml 파일에 설정할 수 있고, 단위는 (초)이다. 기본은 1800. (글로벌 설정은 분 단위로 설정해야 한다. 60, 120, ...)
#application.yaml
server:
  servlet:
    session:
      timeout: 60
  1. 특정 세션 단위로 시간 설정
  • session.setMaxInactiveInterval(1800); //1800초

✔️ 세션 타임아웃 발생

  • 세션의 타임아웃 시간은 세션과 관련된 JSESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화된다.
  • session.getLastAccessedTime() : 최근 세션 접근 시간

✔️ 정리

  • 실무에서 주의할 점은 세션은 최소한의 데이터만 보관해야 한다.
  • 보관한 데이터 용량 * 사용자 수로 세션의 메모리 사용량이 급격하게 늘어나서 장애로 이어질 수 있다.
  • 세션 유지 시간 동안 길게 가져가면 메모리 사용이 누적될 수 있으므로 적당한 시간을 선택해야 한다.
profile
하루하루 꾸준히 포기하지 말고

0개의 댓글