(스프링 mvc2) - 로그인 처리 1 쿠키,세션(1)

짜스의 하루 ·2024년 3월 13일

로그인 요구사항

(로그인 전) 홈 화면

  • 회원 가입
  • 로그인

(로그인 후) 홈 화면

  • 본인 이름(누구님 환영합니다.)
  • 상품 관리
  • 로그 아웃

보안 요구사항

  • 로그인 사용자만 상품에 접근하고, 관리할 수 있음
  • 로그인 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동

프로젝트 생성

패키지 구조

도메인이 가장 중요하다

  • 도메인: UI, 화면, 기술 인프라 등등의 영역은 제외한 시스템이 구현해야 하는 핵심 비지니스 업무 영역을 말한다.
  • 향후 web을 다른 기술로 바꾸어도 도메인은 그대로 유지할 수 있어야 한다.
    --> web은 domain을 알고있지만 domain은 web을 모르도록 설계해야 한다. 이것을 web은 domain을 의존하지만, domain은 web을 의존하지 않는다고 표현한다.

홈 화면

HomeController 수정

@Slf4j
@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }
}

home.html 생성 및 실행

정상적으로 실행되는 것을 확인할 수 있다.


회원 가입

Member 클래스 생성

@Data
public class Member {
    private Long id; //데이터베이스에 저장되는 id

    @NotEmpty
    private String loginId; //회원가입에 입력하는 로그인 ID
    @NotEmpty
    private String name; //사용자 이름
    @NotEmpty
    private String password; //비밀번호
}

MemberRepository 저장소 생성

@Slf4j
@Repository
public class MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    public Member save(Member member){
        member.setId(sequence++);
        log.info("save : member={}", member);
        store.put(member.getId(),member);
        return member;
    }

    public Member findById(long id){
        return store.get(id);
    }

    public Optional<Member> findByLoginId(String loginId){
        return findAll().stream().filter(member -> member.getLoginId().equals(loginId)).findFirst();
    }

    public List<Member> findAll(){
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}

MemberController 생성

@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/add")
    public String addForm(@ModelAttribute("member")Member member){
        return "members/addMemberForm";
    }

    @PostMapping("/add")
    public String save(@Validated @ModelAttribute Member member, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return "members/addMemberForm";
        }
        memberRepository.save(member);
        return "redirect:/";
    }
}
  • 참고 @ModelAttribute("member")를 @ModelAttribute로 변경해도 결과는 같다.
  • 회원 가입 시 검증 오류가 있으면, 다시 회원 가입 페이지로 이동한다.
  • 회원 가입이 정상적으로 완료되면, 홈 화면으로 리다이렉트 한다.

addMemberForm.html 생성
홈 화면에서 회원 가입 클릭시 정상적으로 회원 가입 페이지로 이동한다.

  • 검증 처리 또한 정상적으로 수행됨을 확인할 수 있다.

정상적으로 회원 가입을 했을 때

  • save 로그가 잘 출력됨을 알 수 있다.

회원 정보를 메모리 기반으로 저장하고 있기 때문에, 서버를 재시작 하면, 데이터가 사라진다. 앞으로 편의상 테스트 용 회원 데이터를 추가해보자

회원용 테스트 데이터 추가 - TestDataInit 클래스에 추가

 @PostMapping("/add")
    public String save(@Validated @ModelAttribute Member member, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return "members/addMemberForm";
        }
        memberRepository.save(member);
        return "redirect:/";
    }

로그인 기능

로그인 기능을 개발해보자. 로그인 ID, 비밀번호를 입력하는 부분에 집중해보자

LoginService 생성 - 비지니스 로직

@Service
@RequiredArgsConstructor
public class LoginService {

    private final MemberRepository memberRepository;

    public Member login(String loginId, String password){
        return memberRepository.findByLoginId(loginId)
                .filter(member -> member.getPassword().equals(password))
                .orElse(null);
    }
}
  • 로그인의 핵심 비지니스 로직은 회원을 조회한 다음에 파라미터로 넘어온 password와 비교해서 같으면 회원을 반환하고, 만약 password가 다르면 null를 반환한다.

LoginForm 생성

@Data
public class LoginForm {
    @NotEmpty
    private String loginId;
    @NotEmpty
    private String password;
}

LoginController 생성

@Controller
@RequiredArgsConstructor
@Slf4j
public class LoginController {
    private final LoginService loginService;

    @GetMapping("/login")
    public String loginForm(@ModelAttribute("loginForm") LoginForm form){
        return "login/loginForm";
    }

    @PostMapping("/login")
    public String login(@Validated @ModelAttribute LoginForm form, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return "login/loginForm";
        }
        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
        if(loginMember == null){
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");

            return "login/loginForm";
        }

        //성공 로직 TODO
        return "redirect:/";
    }
}
  • 로그인 컨트롤러는 로그인 서비스를 호출해서 로그인에 성공하면, 홈 화면으로 이동하고, 로그인에 실패하면 bindingResult.reject()를 사용해서 글로벌 오류(ObjectError)를 생성한다.
  • 그리고 정보를 다시 입력하도록 로그인 폼을 뷰 템플릿으로 사용한다.
  • @Validated @ModelAttribute LoginForm form: 폼 데이터를 LoginForm 객체에 바인딩하고 @Validated 어노테이션을 사용하여 유효성 검사를 수행한다.
  • BindingResult bindingResult: 유효성 검사 오류를 캡처하고 보유한다.
  • 유효성 검사 오류가 있는지 확인하고, 오류가 있으면 로그인 폼 뷰("login/loginForm")로 돌아갑니다.
  • 유효성 검사가 성공하면 loginService.login 메서드를 사용하여 로그인을 시도하고, 폼에서 얻은 로그인 ID 및 비밀번호를 전달합니다.
  • bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");: loginFail은 오류 코드, 아이디 또는 비밀번호가 맞지 않습니다.는 오류 메시지이다.

loginForm.html 생성

실행 결과

  • 입력한 로그인 ID와 비밀번호로 가입된 회원이 있으므로, 로그인에 성공하여 홈 화면으로 이동하는 모습이다.

  • 비밀번호를 잘못 입력하자, "아이디 또는 비밀번호가 맞지 않습니다"라는 경고와 함께 다시 로그인 폼이 나타난다.

실행해보면 로그인이 성공하면 홈으로 이동하고, 로그인에 실패하면 "아이디 또는 비밀번호가 맞지 않습니다."라는 경고와 함께 로그인 폼이 나타난다.

로그인의 상태를 유지하면서, 로그인에 성공한 사용자는 홈 화면에 접근시 고객의 이름을 보여주려면 어떻게 해야할까?


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

쿠키를 사용해서 로그인, 로그아웃 기능을 구현해보자

로그인 상태 유지하기
서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아서 브라우저에 전달하면, 브라우저는 앞으로 해당 쿠키를 지속해서 보내준다.

쿠키 생성

  • 웹 브라우저에서 아이디와 비밀번호를 입력해서 로그인을 요청한다(클라이언트에서 서버로 요청)
  • 서버에서는 클라이언트에서 요청한 회원 정보가 회원 저장소에 있는지 확인하여, 유효한 사용자라면, 쿠키를 만들어서 웹 브라우저에 전달한다.
  • 그러면, 웹 브라우저는 내부에 쿠키 저장소라는 곳에서 쿠키 정보를 저장한다.

클라이언트 쿠키 전달

  • 그 다음부터는 해당 도메인 안에서 클라이언트에서 서버로 어떤 페이지를 요청하더라도 항상 쿠키 정보가 포함되서 전송한다

쿠키에는 영속 쿠키과 세션쿠키가 있다

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

브라우저 종료시 로그아웃이 되기를 기대하므로, 우리에게 필요한 것은 세선쿠키 이다.

LoginController - login() 쿠키 생성

Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
        response.addCookie(idCookie);
  • 로그인에 성공하면 쿠키를 생성하고, HttpServletResponse에 담는다
  • 쿠키 이름은 memberId이고, 값은 회원의 id를 담아둔다. 웹 브라우저는 종료 전까지 회원의 id를 서버에 계속 보내줄 것이다.

  • 로그인을 한 후, 개발자 도구를 확인하면, HTTP 응답 헤더에 쿠키가 추가된 것을 확인할 수 있다. --> 그러면 브라우저는 그 다음 요청을 보낼 때 항상 요청 정보에 쿠키 정보를 포함한다.

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";
        
    }
  • @CookieValue를 사용하면 편리하게 쿠키를 조회할 수 있다.
  • 로그인 하지 않은 사용자도 홈에 접근할 수 있기 때문에, required = false를 사용한다.

로직 분석

  • 로그인 쿠키(memberId)가 없는 사용자는 기존 home으로 보낸다.
    --> 로그인 쿠키가 있어도, 회원이 없으면 home으로 보낸다.
  • 로그인 쿠키(memberId)가 있는 사용자는 로그인 사용자 전용 홈 화면인 loginHome으로 보낸다. 추가로 홈 화면에 회원 정보도 출력해야 하기 때문에, member데이터도 모델에 담아서 전달한다.

loginHome.html 생성

실행 결과

  • 로그인에 성공하면, 사용자 이름이 출력되면서, 상품 관리, 로그아웃 버튼을 확인할 수 있다. 로그인에 성공 시 세션 쿠키가 유지되고, 웹 브라우저에서 서버에 요청시 memberId 쿠키를 계속 보내준다.

로그아웃 기능

  • 세션 쿠키이므로 웹 브라우저 종료시
  • 서버에서 해당 쿠키의 종료 날짜를 0으로 지정

LoginController - logout 기능 추가

@PostMapping("/logout")
    public String logout(HttpServletResponse response){
        Cookie cookie = new Cookie("memberId", null);
        cookie.setMaxAge(0);
        response.addCookie(cookie);

        return "redirect:/";
    }
  • 로그아웃도 응답 쿠키를 생성하는데,Max-Age=0를 확인할 수 있다. 해당 쿠키는 즉시 종료된다.

  • 로그아웃시 홈 화면으로 리다이렉트 되고, Response Headers를 보면 Set-Cookie에 Max-Age=0 으로 들어간 것을 확인할 수 있다. (Application 탭에서 보면 쿠키 정보가 없는 것을 확인할 수 있다.)

쿠키만으로 로그인과 로그아웃을 처리하는 방식은 보안상에 큰 문제가 있다.


쿠키와 보안 문제

쿠키를 사용해서 로그인 Id를 전달해서 로그인을 유지할 수 있었지만, 심각한 보안 문제가 있다.

보안문제

  • 쿠키 값은 임의로 변경할 수 있다
  • 쿠키에 보관된 정보는 훔쳐갈 수 있다.
    --> 이 정보가 웹 브라우저에도 보관이 되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달이 된다.
    --> 쿠키의 정보가 나의 로컬 PC가 털릴 수 있고, 네트워크 전송 구간에서 털릴 수도 있다.
  • 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다 (악의적인 요청을 계속 시도할 수 있다)

대안

  • 쿠키에 중요한 값을 놏ㄹ하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출하고, 서버에 토큰과 사용자 id를 매핑해서 인식한다. 서버에서 토큰을 관리한다
  • 토큰은 해커와 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
  • 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료 시간을 짧게 유지한다.(해킹이 의심되는 경우, 서버에서 해당 토큰을 강제 종료하면 된다.)

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

중요한 정보를 모두 서버에 저장해야 한다.
그리고 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결해야 한다.

세션 동작 방식

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

  • 세션 생성
    세션 ID를 생성하는데 추정 불가능 해야 한다
    UUID는 추정이 불가능하다
    생성된 세션 ID와 세션에 보과할 값 (memberA)을 서버의 세션 저장소에보관한다.

  • 세선 id를 응답 쿠키로 전달

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

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

중요

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

클라이언트의 세션 id쿠키 전달

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

정리

  • 세션을 사용해서 서버에서 중요한 정보를 관리하게 되었다.
    1) 쿠카 값을 변조 가능 -> 예상 불가능한 복잡한 세션 ID를 사용했다
    2) 쿠키에 보관하는 정보는 클라이언트 해킹 시 털릴 가능성이 있다 ->세션 Id가 털려도 여기에는 중요한 정보가 없다
    3) 쿠카 탈취 후 사용 -> 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 세션의 만료시간을 짧게(예: 30분) 유지한다. 또는 해킹이 의심되는 경우 서버에서 해당 세션을 강제로 제거하면 된다

로그인 처리하기 - 세션 직접 만들기

세션을 직접 개발해서 적용해보자
세션 생성

  • sessionId 생성 (임의의 추정 불가능한 랜덤 값)
  • 세션 저장소에 sessionId와 보관할 값 저장
  • sessionId로 응답 쿠키를 생성해서 클라이언트에 전달

세션 조회

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

세션 만료

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

먼저 세션을 관리해주는 세션 관리자를 만들고 위 3가지 기능(세션 생성, 세션 조회, 세션 만료)를 적용해보자

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){
        Cookie[] cookies = request.getCookies();
        if(cookies == null){
            return null;
        }
        return Arrays.stream(cookies)
                .filter(cookie -> cookie.getName().equals(cookieName))
                .findAny().orElse(null);

    }
}

SesstionManagerTest생성

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();

    }
}
  • 여기서는 여기서는 HttpServletRequest , HttpservletResponse 객체를 직접 사용할 수 없기 때문에, 테스트에서 비슷한 역할을 해주는 가짜 MockHttpServletRequest , MockHttpServletResponse 를 사용했다.

로그인 처리하기 - 직접 만든 세션 적용

LoginController - loginV2()

  //세션 관리자를 통해 세션을 생성하고, 회원 데이터를 보관
        sessionManager.createSession(loginMember, response);
  • sessionManager.createSession(loginMember,response) : 로그인 성공시 세션을 등록한다. 세션에 loginMember를 저장해두고, 쿠키도 함께 발행한다.

Logincontroller - logoutV2()

  @PostMapping("/logout")
    public String logoutV2(HttpServletRequest request){
        sessionManager.expire(request);
        return "redirect:/";
    }

HomeController - homeLogin

 @GetMapping("/")
    public String homeLogin(HttpServletRequest request, Model model) {
        //세션관리자에 저장된 회원 정보 조회
        Member member = (Member)sessionManager.getSession(request);

        if (member == null) {
            return "home";
        }

        model.addAttribute("member", member);
        return "loginHome";

    }
}
  • 세션 관리자에서 저장된 회원 정보를 조회한다. 만약 회원 정보가 없으면, 쿠키나 세션이 없는 것 이므로 로그인 되지 않은 것으로 처리한다.

실행 결과


HttpSestion 사용

서블릿이 제공하는 HttpSession을 사용해보자
SessionConst 생성

public class SessionConst {
    public static final String LOGIN_MEMBER = "loginMember";
}
  • HttpSession에 데이터를 보관하고 조회할 때, 같은 이름이 중복되어 사용하므로, 실수를 하나 정의했다.

LoginController - loginV3

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

세션 생성과 조회

  • 세션을 생성하려면 request.getSession(true)를 사용하면 된다.

  • public HttpSession getSesstion(boolean create):
    request.getSession(true) : 세션이 있으면 기존 세션 반환, 없으면 새로운 세션 생성해서 반환
    request.getSession(false) : 세션이 있으면 기존 세션 반환, 세션이 없으면 새로운 세션을 생성하지 않고 null반환

  • request.getSession기본 값이 true이다.

세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

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

LoginController - logoutV3()

@PostMapping("/logout")
   public String logoutV3(HttpServletRequest request){
       HttpSession session = request.getSession(false);
       if(session != null){
           session.invalidate();
       }
       return "redirect:/";
   }
  • request.getSesstion(false): 세션 제거가 목적이기 때문에, 새로운 세션을 만들지 않고 기존의 세션만 불러온다
  • session.invalidate() : 세션을 제거한다.

HomeController - homeLoginV3()

 @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";

    }
  • Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
    세션에서 "LOGIN_MEMBER"라는 이름의 속성을 통해 현재 로그인된 사용자 정보를 가져와 Member 객체인 loginMember에 저장하기 위해 캐스팅을 한다.

실행결과

  • JSESSIONID 쿠키가 적절하게 생성되는 것을 확인할 수 있다.

참고

  • request.getSession(false) : request.getSession()를 사용하면 기본값이 true이기 때문에, 로그인 하지 않을 사용자에게도 의미없는 세션이 만들어진다.
    따라서 세션을 찾아야 하는 시점에는 false옵션을 사용해서 새로운 세션을 생성하지 않아야 한다.
  • session.getAttribute(SessionConst.LOGIN_MEMBER) : 로그인 시점에 세션에 보관한 회원 객체를 찾는다.

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

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

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

HomeController - homeLoginV3Spring()

 @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를 포함하고 있는 것을 확인할 수 있다.

  • URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면 다음 옵션을 넣어주면 된다. 이렇게 하면 URL에 jsessionid 가 노출되지 않는다.
  • application.properties
    --> server.servlet.session.tracking-modes=cookie

세션 정보와 타임아웃 설정

세션이 제공하는 정보들을 확인해보자
SessionInfoController 생성

@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("getMaxInactiveInterval={}",session.getMaxInactiveInterval());
        log.info("getCreationTime={}",new Date(session.getCreationTime()));
        log.info("lastAccessedTime={}",new Date(session.getLastAccessedTime()));
        log.info("isNew={}", session.isNew());

        return "세션 출력";
    }
}

세션 출력 결과

  • sessionId :세션Id, JSESSIONID의 값이다.
  • maxInactiveInterval : 세션의 유효 시간 예) 1800초(30분)
  • creationTime : 세션 생성 일시
  • lasstAccessedTime: 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId를 요청한 경우 갱신
  • isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId를 요청해서 조회된 세션인지 여부

세션 타임아웃 설정
세션은 사용자가 로그아웃을 직접 호출해서 session.invalidate()가 호출되는 경우에 삭제된다.그런데, 대부분의 사용자는 로그아웃을 선택하지 그냥 웹 브라우저를 종료한다.

문제는 HTTP가 비연결성이므로 서버 입장에서는 해당 사용자가 웹 브라우저를 종료한 것인지 아닌지를 인식할 수 없다. 따라서 서버에서 세션 데이터를 언제 삭제해야 하는지 판단하기 어렵다.

이 경우 남아있는 세션을 무한정 보관하면 다음과 같은 문제가 발생할 수 있다.

  • 세션과 관련된 쿠키( JSESSIONID )를 탈취 당했을 경우 오랜 시간이 지나도 해당 쿠키로 악의적인 요청을 할 수 있다.
  • 세션은 기본적으로 메모리에 생성된다. 메모리의 크기가 무한하지 않기 때문에 꼭 필요한 경우만 생성해서 사용해야 한다. 10만명의 사용자가 로그인하면 10만개의 세션이 생성되는 것이다.

세션 종료 시점
사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도를 유지해주는 것이다. 이렇게 하면 사용자가 서비스를 사용하고 있으면, 세션의 생존 시간이 30분으로 계속 늘어나게 된다.

세션 타임아웃 설정

  • 스프링 부트로 글로벌 설정
    server.servlet.session.timeout=1800
  • 특정 세션 단위로 시간 설정
    session.setMaxInactioveInterval(1800);

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

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

profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글