- 서버에서 해당 쿠키의 종료 날짜를 0으로 지정한다.
- 로그아웃도 응답 쿠키를 생성하는데 Response Headers를 확인해 보면 `Set-Cookie: Max-Age=0;` 를 확인할 수 있다.
- 또한 Application -> Storage -> Cookies 에 가봐도 쿠키가 삭제된 것을 확인할 수 있다.
쿠키와 보안 문제
위와 같은 구현방식은 엄청 심각한 보안 문제가 있다.
대표적인 문제들은 아래와 같다.
쿠키 값은 임의로 변경할 수 있다.
쿠키에 보관된 정보는 훔쳐갈 수 있다.
해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.
해결 방안
쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출한다.
서버에서 토큰과 사용자 id를 매핑해서 인식하고 서버에서 토큰을 관리한다.
큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다.
서버는 클라이언트에 mySessionId 라는 이름으로 세션ID 만 쿠키에 담아서 전달한다.
클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.
클라이언트는 요청시 항상 mySessionId 쿠키를 전달한다.
서버에서는 클라이언트가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션 정보를 사용한다.
이런식으로 구현하면 쿠키만 사용했을 때 발생할 수 있는 보안 문제를 해결할 수 있다.
하지만 세션에는 최소한의 데이터만 보관해야한다.
객체를 넣지말고, 아이디만 넣는식으로 메모리 관리는 필수이다.
HttpSession 을 통한 로그인 처리
서블릿은 세션을 위해 HttpSession 이라는 기능을 제공해 준다.
로그인 기능 구현
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
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";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession(); //세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
- request.getSession(true)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성해서 반환한다.
- request.getSession(false)
- 세션이 있으면 기존 세션을 반환한다.
- 세션이 없으면 새로운 세션을 생성하지 않는다. -> null 을 반환한다.
- session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember); 을 통해서 세션 정보를 저장한다.
@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) -> true 로 설정할 경우, 로그인 하지 않을 사용자도 의미없는 세션이 만들어진다.
session.getAttribute(SessionConst.LOGIN_MEMBER) -> 로그인 시점에 세션에 보관한 회원 객체를 찾는다.
위와 같은 기능을 스프링에서 제공하는 @SessionAttribute를 사용하면 더 간단하게 구현이 가능하다.
@GetMapping("/")
public String homeLoginV3Spring( @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
이 기능은 세션을 생성하지 않는다.
세션 제공하는 정보
@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 의 값이다. -> ex> 34B14F008AA3527C9F8ED620EFD7A4E1
maxInactiveInterval : 세션의 유효 시간 -> ex> 1800초, (30분)
creationTime : 세션 생성일시
lastAccessedTime :세션과 연결된 사용자가 최근에 서버에 접근한 시간이다.
클라이언트에서서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.
isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로
sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부
세션 타임아웃 설정
세션은 사용자가 로그아웃을 직접 호출해서 session.invalidate() 가 호출 되는 경우에 삭제된다.
하지만 대부분의 사용자는 로그아웃을 하지 않고 종료한다. -> 그렇기 때문에 세션에 타임아웃을 꼭 설정해 줘야한다.
세션의 종료시점은 최근에 요청한 시간을 기준으로 30분 정도를 유지해주는 것이 좋다.
설정 방법
글로벌 -> application.properties 파일에 server.servlet.session.timeout=1800 를 적어준다.
특정 세션 단위로 시간 설정 -> session.setMaxInactiveInterval(1800);