WEEK 6-8: Spring Session

ensalada.de.pollo·2025년 5월 18일

be

목록 보기
30/44

Session

Cookie를 사용한 인증 및 인가 방식은 보안 문제가 많습니다. 중요한 정보를 클라이언트에 저장하면, 변조 또는 탈취 등 보안 문제가 발생합니다. 그렇기 때문에, 중요한 정보는 서버 측에 저장하고, 클라이언트와는 예측이 불가능한 임의의 값으로 연결하는 방식이 필요합니다.

동작 원리

사용자가 로그인에 성공하면, 서버는 임의의 SessionId(예측 불가능한 값)을 생성합니다. 이 SessionId와 로그인한 유저 정보를 서버의 session 저장소에 저장합니다.
서버는 HTTP Response의 set-cookie 헤더로 SessionId=값 을 브라우저에 전달합니다. 그리고 브라우저는 해당 쿠키(Session Id)를 저장합니다.

로그인 이후에는, 클리언트가 모든 요청에 쿠키(Session Id)를 자동으로 포함합니다. 서버는 전달받은 Session Id로 Session 저장소에서 사용자 정보를 조회합니다. 인증 및 인가 등 필요한 처리를 수행합니다.

로그아웃 시에는 해당 세선을 삭제합니다. 보통 일정 시간동안 요청이 없으면 세션을 만료시켜 자동 삭제합니다.

특징

  • 중요한 정보는 서버에만 저장하기 때문에, 해당 값은 변조해도 무의미합니다.
  • Session Id에는 중요한 정보가 없고 단순한 식별자 역할을 하며, 탈취해도 서버에 정보가 없으면 무의미합니다.
  • Session Id는 예측 불가능한 값이고, 쿠키의 HttpOnly/Secure 속성을 활용해 보안 측면에서도 Cookie 방식보다 유리합니다.
  • Session은 일정시간 후 만료되며, 해킹이 의심될 시 바로 삭제가 가능합니다.
  • Session은 서버 메모리와 DB 등에 저장되기 때문에, 대규모 서비스에서는 세션 저장소의 관리가 필요합니다.

Servlet의 HttpSession

HttpSession은 Servlet에서 공식적으로 지원하는 Session이며, Session 구현에 필요한 다양한 기능들을 지원합니다.

주요 메서드

  • request.getSession(true): 세션이 있으면 반환, 없으면 새로 생성하여 반환
  • request.getSession(false): 세션이 있으면 반환, 없으면 null 반환
  • session.setAttribute(key, value): 세션에 데이터 저장
  • session.getAttribute(key): 세션 데이터 조회
  • session.invalidate(): 세션 무효화(삭제)

상수를 클래스로 관리하는 방법

// 추상클래스 -> O
public abstract class Const {
	public static final String LOGIN_USER = "loginUser";
}

// 인터페이스 -> O
public interface Const {
	String LOGIN_USER = "loginUser";
}

// 클래스 -> X
public class Const { // new Const();
	public static final String LOGIN_USER = "loginUser";
}

// Const.LOGIN_USER; O
// new Const(); X

상수로 활용할 class는 인스턴스를 생성하지 않습니다. 그렇기 때문에 abstract 추상 클래스 혹은 interface로 만들어 상수 값만 사용하도록 만들어두면 됩니다.

Servlet으로 session을 생성할 때?

Servlet을 통해 HttpSession을 생성하게 되면, SessionId가 JSESSIONID로 생성이 되고, 이값은 예측 불가능한 랜덤 값으로 생성이 됩니다.

예시

로그인 컨트롤러

@PostMapping("/session-login")
public String login(
        @Valid @ModelAttribute LoginRequestDto dto,
        HttpServletRequest request
) {

    LoginResponseDto responseDto = userService.login(dto.getUserName(), dto.getPassword());
    Long userId = responseDto.getId();

    // 실패시 예외처리
    if (userId == null) {
        return "session-login";
    }

    // 로그인 성공시 로직
    // Session의 Default Value는 true이다.
    // Session이 request에 존재하면 기존의 Session을 반환하고,
    // Session이 request에 없을 경우에 새로 Session을 생성한다.
    HttpSession session = request.getSession();

    // 회원 정보 조회
    UserResponseDto loginUser = userService.findById(userId);

    // Session에 로그인 회원 정보를 저장한다.
    session.setAttribute(Const.LOGIN_USER, loginUser);

    // 로그인 성공시 리다이렉트
    return "redirect:/session-home";
}

로그아웃 컨트롤러

@PostMapping("/session-logout")
public String logout(HttpServletRequest request) {
    // 로그인하지 않으면 HttpSession이 null로 반환된다.
    HttpSession session = request.getSession(false);
    // 세션이 존재하면 -> 로그인이 된 경우
    if(session != null) {
        session.invalidate(); // 해당 세션(데이터)을 삭제한다.
    }

    return "redirect:/session-home";
}

@SessionAttribute

컨트롤러 메서드 파라미터에 붙여 세션에 저장된 값을 쉽게 꺼낼 수 있게 해주는 Spring annotation입니다. request.getSession(true)처럼 새롭게 세션을 생성하는 것이 아닌, 이미 존재하는 세션에서만 값을 꺼냅니다.

주로, 이미 로그인이 완료된 사용자를 찾는 것과 같은 Session이 이미 있는 경우에 사용합니다.

@Controller
@RequiredArgsConstructor
public class SessionHomeController {
    private final UserService userService;

    @GetMapping("/v2/session-home")
    public String homeV2(
            @SessionAttribute(name = Const.LOGIN_USER, required = false) UserResponseDto loginUser,
            Model model
    ) {
        // 세션에 loginUser가 없으면 로그인 페이지로 이동
        if (loginUser == null) {
            return "session-login";
        }
        // 세션에 값이 있으면 로그인된 것으로 간주
        model.addAttribute("loginUser", loginUser);
        return "session-home";
    }
}

required=false로 지정하면, 세션이 없어도 에러가 발생하지 않고, null이 들어옵니다. 세션이 없거나 값이 없으면 로그인 페이지로 이동하는 로직에 적합합니다.

JSESSIONID

사용자가 처음 로그인하면, 서버가 임의의 JSESSIONID를 생성해 쿠키로 브라우저에 전달합니다. 브라우저는 이후 모든 요청에 JSESSIONID를 자동적으로 포함하게 됩니다.

쿠키를 지원하지 않는 환경에서는 JSESSIONID가 URL 파라미터로 전달될 수 있지만, 보통 사용하지 않는 방법입니다. Thymeleaf와 같은 템플릿 엔진은 자동으로 jsessionId를 URL에 붙여줄 수 있습니다.

쿠키로만 세션을 유지하고 싶은 경우에는 설정파일(application.properties 또는 application.yml)에 다음과 같은 설정을 넣어주면 됩니다.
server.servlet.session.tracking-modes=cookie

HttpSession에서의 세션 정보 조회

HttpSession 객체는 세션 정보를 다양하게 제공합니다.

@Slf4j
@RestController
public class SessionController {
    @GetMapping("/session")
    public String session(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return "세션이 없습니다.";
        }
        log.info("session.getId()={}", session.getId());
        log.info("session.getMaxInactiveInterval()={}", session.getMaxInactiveInterval());
        log.info("session.getCreationTime()={}", session.getCreationTime());
        log.info("session.getLastAccessedTime()={}", session.getLastAccessedTime());
        log.info("session.isNew()={}", session.isNew());
        return "세션 조회 성공!";
    }
}

주요 메서드는 아래와 같습니다.

  • session.getId()
    현재 세션의 jsessionId값을 조회
  • session.getMaxInactiveInterval()
    세션의 유효시간을 지정
  • session.getCreationTime()
    세션 생성 시각
  • session.getLastAccessedTime()
    해당 세션에 마지막으로 접근한 시각
  • session.isNew()
    새로 생성된 세션인지 여부

Session의 문제점

  • HTTP는 connectionless 특성을 가지기 때문에 서버가 브라우저 종료 여부를 판별하지 못합니다. 그렇기 때문에 서버 입장에서는 session을 언제 삭제해야하는지 판단하기 힘들어집니다.
  • JSESSIONID의 값을 탈취당하면 해당 값으로 악의적인 요청을 보낼 수 있습니다.
  • Session은 서버 메모리에 생성이 됩니다. 하지만 자원은 한정적이므로 꼭 필요한 경우에만 생성해야 합니다.

Session Timeout

Session은 로그아웃 기능을 사용해 session.invalidate()가 되어야 삭제되지만, 보통 사용자들은 로그아웃을 하지 않고, 브라우저를 종료합니다.

Session은 기본적으로 30분을 기준으로 하여 삭제됩니다. 여기서 문제점은, 실제 로그인 후 30분 이상의 시간 동안 사용 중인 사용자의 session도 삭제가 됩니다. 이렇게 되면 사용자 입장에서는 다시 로그인해야 하는 번거로움이 발생합니다.

HttpSession은 이러한 점을 보완하기 위해서 session의 생성 시점을 기준이 아닌, 마지막 요청을 기준으로 30분을 유지합니다. 기본적으로 해당 방식으로 session의 생명주기를 관리하며, session 정보에서 LastAccessedTime을 기준으로 30분이 지나면 WAS가 내부적으로 세션을 삭제합니다.

Session의 한계

Session은 서버의 메모리를 사용하기 때문에, 확장성이 제한되어 있습니다.

동시 접속 사용자가 많아지는 경우, 서버 메모리의 사용량이 급증합니다. 그렇기 때문에, 서버는 session에 꼭 필요한 최소 데이터만 저장해야 하고, 세션 유지 시간을 너무 길게 한다면, 리소스 부족 문제가 발생할 수 있습니다.

그리고 세션은 서버별로 관리가 되기 때문에 여러 대의 서버(scale out)에서 세션을 공유하기가 어렵습니다. 예를 들어서 서버1에서 로그인한 사용자가 서버2에 요청을 보내면 세션 정보가 없기 때문에 로그아웃된 상태가 됩니다. 이를 해결하기 위해선는Sticky Session이나 세션 클러스터링 또는 redis와 같은 외부 저장소를 사용하지만 복잡성과 오버헤드가 증가합니다.

JSESSIONID가 탈취되면, 세션이 만료될 때까지 악의적 사용이 가능합니다. 만료 정책과 함께 필요시 강제 만료 기능도 구현해야합니다.

속성CookieSession
저장 위치클라이언트(브라우저)서버(메모리 또는 DB)
수명만료일 지정 가능, 영속/세션 cookie 구분브라우저 종료, 로그아웃, 타임아웃 시 만료
용도방문 기록, 로그인 유지, 맞춤 설정, 광고 등로그인 정보, 사용자 상태 등
보안변조 및 탈취 위험, 민감 정보 저장 금지서버에 저장되어있어 상대적으로 안전
확장성서버에 부담은 없음서버 메모리 부담, 대규모 서비스 확장성에서의 한계

인증(Authentication)과 인가(Authorization)

인증(Authentication)은 사용자가 누구인지 확인하는 과정이고, 인가(Authorization)은 인증된 사용자가 어떤 권한을 가지는지 결정하는 과정입니다.

인가는 인증이 반드시 선행되어야하고, 이 둘의 개념을 구분하여 알고 있어야 합니다!

자료 및 코드 출처: 스파르타 코딩클럽

0개의 댓글