[Spring] 로그인 처리1 - 쿠키, 세션

bien·2023년 8월 2일
0

Spring_MVC2

목록 보기
2/7

로그인 처리하기 - 쿠키사용

로그인 상태 유지하기

로그인의 상태를 어떻게 유지할 수 있을까? 쿼리 파라미터를 계속 유지하면서 보내는 것은 매우 어렵고 번거로운 작업이다. 쿠키를 사용해보자!

쿠키

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

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

쿠키 = 영속쿠키 & 세션 쿠키

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

LoginController - login()

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

        if (bindingResult.hasErrors()) {
            return "admin/loginForm";
        }

        AdminDTO findAdmin = loginService.loginAdmin(loginDTO.getId(), loginDTO.getPassword());
        log.info("findAdmin = {}", findAdmin);

        // 로그인이 실패한 경우 alert 후 다시 로그인 페이지 제공
        if (findAdmin == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "admin/loginForm";
        }

        // 로그인 성공 처리
        Cookie idCookie = new Cookie("adminId", loginDTO.getId());
        response.addCookie(idCookie);
        return "redirect:/";
    }

쿠키 생성 로직

        Cookie idCookie = new Cookie("adminId", loginDTO.getId());
        response.addCookie(idCookie);

로그인 성공 시, 1. 쿠키를 생성하고 2. HttpServletResponse에 담는다. 웹브라우저는 종료 전까지 회원의 id를 서버에 계속 보내 줄것이다.

controller 로그인 처리

    @GetMapping("/")
    public String homeLogin(@CookieValue(name = "adminId", required = false) String adminId, Model model) {

        if(adminId == null) {
            log.info("adminId가 null입니다.");
            log.info("adminID={}", adminId);
            return "home";
        }

        // 쿠키와 일치하는 id가 없는 경우, 다시 비로그인 홈으로 이동합니다.
        AdminDTO loginAdmin = adminService.findAdminByAdminId(adminId);
        if(loginAdmin == null) {
            log.info("조회한 loginAdmin이 null 입니다");
            return "home";
        }

        model.addAttribute("admin", loginAdmin);
        log.info("loginHome으로 이동");
        return "loginHome";
    }

@CookieValue

  • @CookieValue로 편리하게 쿠키를 조회할 수 있다.
  • 로그인하지 않은 사용자도 접근 가능하도록 required = false를 사용한다.

LoginController - logout

	Cookie cookie = new Cookie(cookieName, null);
    cookie.setMaxAge(0);
    response.addCookie(cookie);

로그아웃은 응답 쿠키를 생성하면서 Max-Age=0를 설정하여 수행한다. 해당 쿠키는 즉시 종료된다.


쿠키와 보안 문제

보안 문제

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

  • 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.
  • F12 > Application > Cookie 실제로 변경 가능하다.

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

  • 쿠키에 개인정보, 신용카드 정보가 있다면? 이 정보가 웹 브라우저에 보관되고, 네트워크 요청마다 계쏙 클라이언트에서 서버로 전달된다.
  • 중요한 개인 정보가 로컬 PC에서 털리거나 네트워크 전송 구간에서 털릴 수 있다.

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

  • 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 계속 시도할 수 있다.

대안

  • 쿠키에 중요한 값을 노출하지 않는다.
    • 대신, 예측 불가능한 임의의 토큰(랜덤 값)을 노출한다.
    • 서버에서 토큰과 사용자 ID를 매핑해서 인식한다.
  • 토큰은 예상 불가능해야 한다.
    • 해커가 임의의 값을 넣어도 찾을 수 없도록.
  • 토큰의 만료시간이 짧아야 한다.(예: 30분)
    • 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록.
    • 해킹이 의심되는 경우 서버에서 토큰을 강제 제거하면 된다.

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

😥사용자를 식별하기 위해 쿠키에 인식 요소를 사용하는건데, 쿠키에 중요한 정보를 보관하면 안되겠네...
🤔 그럼 1.중요한 정보는 모두 서버에 저장해야 하고, 2.서버에서 식별에 이용되는 임의의 식별값이 추정 불가능해야 겠구나!

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

세션 동작 방식

  • 로그인: 사용자가 id, password 정보를 전달하면 서버에서 해당 사용자가 맞는지 확인한다.

  • 세션 ID를 생성하는데, 이는 추정 불가능해야 한다.
    -Cookie: mySessionId=zz0101xx-bab9-4b92-9b32-dadb280f4b61
  • 생성된 세션 ID와 세션에 보관할 값(memberA)을 서버의 세션 저장소에 보관한다.

  • 세션 ID를 응답 쿠키로 전달한다.

클라이언트와 서버는 결국 쿠키로 연결이 되어야 한다.

  • 서버는 클라이언트에 mySessionId라는 이름으로 세션 ID만 쿠키에 담아서 전달한다.
  • 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.

keyPoint

  • 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않는다.
  • 오직 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에 전달한다.

  • 클라이언트는 요청 시 항상 mySessionId쿠키를 전달한다.
  • 서버에서는 클라이언트가 전달한 mySessionId쿠키 정보로 세션 정보를 조회해서 로그인시 보관한 세션 정보를 사용한다.

정리

세션을 이용해 이제는 서버에서 중요한 정보를 관리한다. 따라서 다음과 같은 문제도 해결 가능하다.

  • 쿠키 값 변조 가능성
    • => 예상 불가능한 복잡한 세션 ID 사용
  • 쿠키에 보관하는 정보(클라이언트 쪽) 털릴 가능성
    • => 세션 ID가 털려도 중요한 정보가 없다. (털려봐야 상관이 없다.)
  • 쿠키 탈취 후 사용
    • => 해커가 토큰을 털어가도, 서버에서 세션 만료시간을 짧게 유지하면 의미가 없다.

로그인 처리하기 - 직접 만든 세션 등록

//TODO 추가


로그인 처리하기 - 서브릿 HTTP 세션 1

서블릿은 세션을 위해 HttpSession이라는 기능을 제공하고 있다.

HttpSession

서블릿을 통해 HttpSession을 생성하면 다음과 같은 쿠키를 생성한다. 이름은 JESSIONID이고, 값은 추정 불가능한 랜덤 값이다.

Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05

로그인 로직

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

📗 세션 생성과 조회

public HttpSession getSession(boolean create);
  • request.getSession(true) (기본값)
    • 세션 有 : 기존 세션을 반환
    • 세션 無 : 새로운 세션 생성해서 반환
  • request.getSession(false)
    • 세션 有 : 기존 세션을 반환
    • 세션 無 : 새로운 세션 생성 안함. null 반환

📗 세션에 로그인 회원 정보 보관

session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

세션에 데이터 보관하는 방법은 request.setAttribute(..)와 비슷하다. 하나의 세션에 여러 값을 저장할 수 있다.

뷰 렌더링 로직

    @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";
    }
  • request.getSession(false)
    • request.getSession() 를 사용하면 기본 값이 create: true 이므로, 로그인 하지 않을 사용자도 의미없는 세션이 만들어진다.
    • 따라서 세션을 찾아서 사용하는 시점에는 create: false 옵션을 사용해서 세션을 생성하지 않아야 한다.
  • session.getAttribute(SessionConst.LOGIN_MEMBER)
    • 로그인 시점에 세션에 보관한 회원 객체를 찾는다.

😣아.. 이거 session에 값 있는지 확인하고, 또 꺼내서 비교하고, 이런거 귀찮은데 더 간단한 방법이 없나?
😉 그럴때, @SessionAttribute를 사용할 수 있습니다!


로그인 처리하기 - 서브릿 HTTP 세션 2

📗 @SessionAttribute

스프링이 제공하는 세션 기능

@SessionAttribute(name = "loginMember", required = false) Member loginMember
  • 이미 로그인 된 사용자를 찾을때 사용
  • 세션을 새로 생성하지 않는다.

HomeController

    @GetMapping("/")
    public String homeLogin(@SessionAttribute(name = SessionConst.LOGIN_ADMIN, required = false) AdminDTO loginAdmin,
                            Model model) {

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

        // 세션이 유지되면 loginHome으로 이동
        model.addAttribute("admin", loginAdmin);
        return "loginHome";
    }

세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 한번에 편리하게 처래히준다.

📗 TrackingModels

로그인을 처음 시도하면 urL이 jessionid를 포함하고 있다.

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

이는 웹 브라우저가 쿠키를 지원하지 않을때, 쿠키 대신 URL을 통해 세션을 유지하라는 용도로 서버에서 이 값을 제공한다.(이 방법을 위해선 url에 계속 이 값을 포함해서 전달해야 한다) 서버입장에서는 웹 브라우저가 쿠키를 지원하는지 하지 않는지 최초에는 판단하지 못하므로, 쿠키 값도 전달하고, URL에 jessionid도 함게 전달하는 것이다.

URL 전달 방식을 끄고 항상 쿠키를 통해서면 세션을 유지하고 싶다면, 다음 옵션을 넣어주면 된다.

application.properties

server.servlet.session.tracking-modes=cookie

세션 정보와 타입아웃 설정

세션 정보 확인

SessionInfoController

package com.study.admin.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Date;

@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, JESSIONID의 값.
    • 예) 34B14F008AA3527C9F8ED620EFD7A4E1
  • maxInactiveInterval: 세션의 유효 시간
    • 예) 1800초 (30분)
  • createTime: 세션 생성 일시
  • lastAccessedTime: 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionID를 요청한 경우에 갱신된다.
  • isNew: 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId를 요청해서 조회된 세션인지 여부

세션 타임아웃 설정

  • 세션은 사용자가 로그아웃을 호출하여 session.invalidate()가 호출되는 경우에만 삭제된다.
  • 그런데 대부분의 사용자는 로그아웃을 하지 않고, 그냥 웹 브라우저를 종료한다.
  • HTTP는 비연결성(ConnectionLess)이므로 서버 입장에서는 해당 사용자가 웹 브라우저 종료 여부를 인식할 수 없다.
  • 따라서, 서버 입장에서는 세션 데이터 삭제 타이밍을 알기 어렵다.

만약 세션이 무한정 보관된다면 어떤 문제점들이 발생할까?

  • 세션과 관련된 쿠키를 탈취당했을 경우, 해당 쿠키로 악의적 요청이 가능
  • 세션은 기본적으로 메모리에 생성. 10만명의 사용자 로그인시 10만명의 세션이 생성되게 된다. 메모리의 크기가 무한하지 않으므로, 이는 서버에 큰 부담이 될 수 있다.

세션 종료 시점

🤔그럼 세션 종료 시점을 언제로 정하면 좋을까?

한 30분 정도 유지하는것이 좋을것 같다.

  • 그러나 한참 웹 사이트를 사용하고 있는데, 30분 뒤에 로그아웃 되며 다시 로그인을 하라는 요청이 들어오면 사용자 입장에서는 화가 날 것이다. 😡
  • 따라서, 세션 생성 시점이 아니라 사용자가 최근에 서버에 요청한 시간으로 부터 30분 정도를 유지하는 것이 적절할 듯 하다.
    • 사용자가 서비스를 사용중이면, 세션의 생존 기간이 30분으로 늘어난다.
    • HttpSession은 이 방식을 사용하고 있다.

세션 타임아웃 설정

  • 스프링부트로 글로벌 설정

application.properties

server.servlet.session.timeout=60

기본은 1800(30분)
최소값 60

  • 특정 세션 단위로 시간 설정
session.setMaxInactiveInterval(1800); //1800초

세션 타임아웃 발생

세션의 타임아웃 시간은 해당 세션과 관련된 JESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화된다. 초기화 되면, 타임아웃으로 설정한 시간만큼 세션을 추가로 사용할 수 있다.

session.getLastAccessedTime(): 최근 세션 접근 시간
LastAccessedTime 이후로 timeout시간이 지나면, WAS가 내부에서 해당 세션을 제거한다.

정리

세션에는 최소한의 데이터만 보관해야 한다. 이를 주의하지 않으면 (보관한 데이터 용량 * 사용자 수)로 세션의 메모리 사용량이 급격히 늘어나 장애로 이어질 수 있다.

또한 세션의 시간을 너무 길게 가져가면 메모리 사용이 계속 누적될 수 있으므로 적당한 시간을 선택하는 것이 필요하다. (기본은 30분)

profile
Good Luck!

0개의 댓글