쿠키의 기본 개념은 생략하고,
쿠키에는 영속 쿠키와 세션 쿠키가 있다.
영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지
세션 쿠키 : 만료 날짜 생략하면 브라우저 종료 시까지만 유지
쿠키 생성
- 다음과 같이 생성하면 된다. 생성한 쿠키는 HttpServletResponse에 담으면 된다.
- 생성자는 (쿠키 이름, 값) 형태다.
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
쿠키 조회
- @CookieValue로 조회 가능하다.
- 로그인하지 않은 사용자도 해당 url에 접근할 수 있게 하려면 required 설정을 꼭 false로 설정해야 한다.
- 예시(쿠키 존재 여부를 통해 로그인 여부 확인)
@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";
}
쿠키 삭제
- 세션 쿠키의 경우 웹 브라우저가 종료될때 삭제되거나,
- 서버에서 해당 쿠키의 종료 날짜를 0으로 설정하여 삭제할 수 있다.
- 예시(로그아웃 시 쿠키 삭제하기)
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
쿠키의 보안 문제
1. 쿠키 값이 임의로 변경될 수 있음
- 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 됨
- 웹브라우저의 개발자모드 -> Application -> 쿠키 변경으로 확인 가능
2. 쿠키 정보는 도난될 수 있음
- 그래서 쿠키에 중요 정보를 넣지 않는 방법을 사용해야 함
3. 해커가 쿠키를 한번 훔치면 평생 사용 가능
- 훔친 쿠키로 계속 악의적인 요청을 시도할 수 있다.
대안은?
- 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출하고, 서버에서 토큰과 사용자 id를 매핑해서 인식한다. 그리고 서버에서 토큰을 관리한다.
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
- 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게(예: 30분) 유지한다. 또는 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 된다.
=> 대안은 세션이다!
세션의 동작 방식
- 쿠키를 주고받을 때 세션id를 주고받는다. 이때 세션id는 추정 불가능해야 한다.
- 서버는 세션 저장소를 만들어, 세션id와 값을 보관한다.
- 이렇게 하면 쿠키에는 중요 정보를 담지 않을 수 있다. 오직 추정 불가능한 id만을 주고받게 된다.
문제 해결
- 쿠키 값 변조 가능 -> 예상 불가능한 복잡한 세션 id 사용
- 쿠키에 중요 정보가 들어갈 수 있다 -> 세션 id가 털려도 중요 정보를 알아낼 수 없다.
- 탈취한 쿠키 무기한 사용 -> 세션 만료 시간(보통 30분)을 설정하여 짧게 유지
세션의 생성, 조회, 만료 기능 직접 만들기
- 아래의 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) {
if (request.getCookies() == null) {
return null;
}
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
}
서블릿 HTTP 세션
- 세션 관리 클래스를 그럼 매번 직접 만들어야 하나? NO
- 서블릿은 세션을 위해 HttpSession이라는 기능을 제공해줌
- 직접 만든 것보다 더 다양한 기능을, 더 쉽게 제공 => 결론은 이걸 쓰자..!
서블릿 HTTP 세션 사용법
<생성>
- request.getSession()를 사용하면 됨
public HttpSession getSession(boolean create);
- 옵션으로 create값을 설정 가능(default는 true)
true : 세션이 있으면 기존 세션 반환, 세션이 없으면 새 세션 생성하여 반환
false : 세션이 있으면 기존 세션 반환, 세션이 없으면 새 세션 생성하지않고 null 반환
<세션에 데이터 보관>
- session.setAttribute()를 사용하면 됨
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
<세션 제거>
session.invalidate()
서블릿 HTTP 세션 - @SessionAttribute
스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute 을 지원한다.
이미 로그인 된 사용자를 찾을 때는 다음과 같이 사용하면 된다. 참고로 이 기능은 세션을 생성하지 않는다. @SessionAttribute(name = "loginMember", required = false) Member loginMember
- 활용 전
@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);
// 세션에 회원 데이터가 없으면 홈으로 이동
if (loginMember == null) {
return "home";
}
// 세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
- 활용 후
@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 를 포함하고 있는 것을 확인할 수 있다.
http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872
이것은 웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법이다. 이 방법을 사용하려면 URL에 이 값을 계속 포함해서 전달해야 한다. 타임리프 같은 템플릿은 엔진을 통해서 링크를 걸면 jsessionid 를 URL에 자동으로 포함해준다. 서버 입장에서 웹 브라우저가 쿠키를 지원하는지 하지 않는지 최초에는 판단하지 못하므로, 쿠키 값도 전달하고, URL에 jsessionid 도 함께 전달한다.
URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶으면 다음 옵션을 넣어주면 된다. 이렇게 하면 URL에 jsessionid 가 노출되지 않는다.
//application.properties
server.servlet.session.tracking-modes=cookie
세션 타임아웃 설정
- 세션이 제공하는 다양한 정보들
@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 의 값이다. 예) 34B14F008AA3527C9F8ED620EFD7A4E1
maxInactiveInterval : 세션의 유효 시간, 예) 1800초, (30분)
creationTime : 세션 생성일시
lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.
isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부
<타임아웃 설정법>
- 로그아웃 시에 세션이 만료되도록 설정을 하면, 대부분의 사용자는 로그아웃을 하지 않고 그냥 웹 브라우저를 종료하기 때문에 서버 입장에서는 종료 여부를 알 수 없다. 그렇다고 세션을 무한정 보관하면 문제가 생길 수 있다. (해커가 탈취한 쿠키를 계속 사용, 또는 메모리 문제)
- 글로벌 설정
//application.properties
server.servlet.session.timeout=60 : 60초, 기본값은 1800(30분)
- 특정 세션에만 설정
session.setMaxInactiveInterval(1800); //1800초
----------------------------------------------
참고 : 인프런 김영한님 강의(스프링 MVC 2편 - 백엔드 웹 개발 활용 기술)
댓글