앞서 쿠키를 사용했을 때의 문제점을 해결하기 위해서는
이렇게 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라고 한다!
사용자가 loginId
, password
정보를 전달하면 서버에서 해당 사용자가 맞는지 확인한다.
memberA
)을 서버의 세션 저장소에 보관한다. mySessionId
라는 이름으로 세션ID만 쿠키에 담아서 전달한다.mySessionId
쿠키를 보관한다.⭐ 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않는다!
오직 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에 전달한다.
mySessionId
쿠키를 전달한다.mySessionId
쿠키 정보로 세션 저장소를 조회해서 로그인 시 보관한 세션 정보를 사용한다.세션을 사용해서 서버에서 중요한 정보를 관리할 수 있으며, 다음과 같은 보안 문제들을 해결할 수 있게 되었다!
세션을 직접 개발해서 적용해보자!
📌 세션 관리
- 세션 생성
- sessionId 생성 (임의의 추정 불가능한 랜덤 값)
- 세션 저장소에 sessionId와 보관할 값 저장
- sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
- 세션 조회
- 클라이언트가 요청한 sessionId 쿠키의 값으로 세션 저장소에 보관한 값 조회
- 세션 만료
- 클라이언트가 요청한 sessionId 쿠키의 값으로 세션 저장소에 보관한 sessionId와 값 제거
🔗 전체 코드
SessionManager
HashMap
은 동시 요청에 안전하지 않아서, 동시 요청에 안전한 ConcurrentHashMap
을 사용하였다.SessionManagerTest
HttpServletRequest
, HttpServletResponse
객체를 직접 사용할 수 없기 때문에,MockHttpServletRequest
, MockHttpServletResponse
를 사용했다.지금까지 개발한 세션 관리 기능을 실제 프로젝트에 적용해보자!
LoginController - loginV2()
@PostMapping("/login")
public String loginV2(@Valid @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";
}
//로그인 성공 처리
//세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
sessionManager.createSession(loginMember, response);
loginMember
를 저장해두고, 쿠키도 함께 발행한다.LoginController - logoutV2()
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
로그아웃 시 해당 세션의 정보를 제거한다.
→ 로그인할 때마다 다른 Value가 저장되는 것을 확인할 수 있다!
서블릿은 세션을 위해 HttpSession
이라는 기능을 제공하는데, 지금까지 나온 문제들을 해결해준다!😃
HttpSession
소개서블릿이 제공하는 HttpSession
도 결국 우리가 직접 만든 SessionManager
와 같은 방식으로 동작한다.
서블릿을 통해 HttpSession
을 생성하면 이름이 JSESSIONID
이고, 추정 불가능한 랜덤 값을 가진 쿠키가 생성된다!
→ Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05
HttpSession
사용하기세션을 생성하려면 request.getSession(true)
를 사용하면 된다. 이때 true
는 create
옵션 값 중 하나이다.
📌
create
옵션
request.getSession(true)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성해서 반환한다.
request.getSession(false)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성하지 않는다.
null
을 반환한다.
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
세션에 데이터를 보관하는 방법은 request.setAttribute(...)
와 비슷하다.
하나의 세션에 여러 값을 저장할 수 있다.
로그인할 때마다 JSESSIONID
가 변경된다!
@SessionAttribute
스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute
를 제공한다.
이미 로그인 된 사용자를 찾을 때는 다음과 같이 사용하면 된다. 이 기능은 세션을 생성하지 않는다.
@SessionAttribute(name = "loginMember", required = false) Member loginMember
TrackingModes
로그인을 처음 시도하면 URL이 다음과 같이 jsessionid
를 포함한다.
이것은 웹 브라우저가 쿠키를 지원하지 않을 때, 쿠키 대신 URL을 통해서 세션을 유지하는 방법!
이 방법을 사용하려면 URL에 이 값을 계속 포함해서 전달해야 한다.
만약 URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면 application.properties
에 다음 코드를 추가해주면 된다.
server.servlet.session.tracking-modes=cookie
이렇게하면 URL에 jsessionid
가 노출되지 않는다.
sessionId
: 세션Id, JSESSIONID
의 값maxInactiveInterval
: 세션의 유효 시간creationTime
: 세션 생성일시lastAccessedTime
: 세션과 연결된 사용자가 최근에 서버에 접근한 시간. sessionId
(JSESSIONID
)를 요청한 경우에 갱신됨.isNew
: 새로 생성된 세션인지, 아니면 이미 과거에 만들어져서sessionId
(JSESSIONID
)를 요청해서 조회된 세션인지 여부세션은 사용자가 로그아웃을 직접 호출해서 session.invalidate()
가 호출되는 경우에 삭제된다.
그런데 대부분 로그아웃을 선택하지 않고, 그냥 웹 브라우저를 종료한다.
문제는 서버 입장에서는 해당 사용자가 웹 브라우저를 종료한 것인지 아닌지를 인식할 수 없다.
(HTTP가 비연결성(ConnectionLess
)이기 때문!)
그래서 서버에서 세션 데이터를 언제 삭제해야 하는지 판단하기가 어렵다😥
이 경우 남아있는 세션을 무한정 보관하면 다음과 같은 문제가 발생할 수 있다.
JSESSIONID
)를 탈취당했을 경우,이런 이유로, 세션은 타임아웃을 설정해야 한다!
사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도를
유지해주자!
이렇게 하면 사용자가 서비스를 사용하고 있으면, 세션의 생존 시간이 30분으로 계속 늘어나게 되어서 30분 마다 로그인해야 하는 번거로움이 사라진다. HttpSession
은 이 방식을 사용한다!
스프링 부트에서는 application.properties
에 글로벌 설정을 해 줄 수 있다.
session.setMaxInactiveInterval(1800); //1800초