쿠키와 세션은 보통 로그인, 로그아웃을 구현할 때 주로 사용된다.
예를 들어, 사용자가 웹사이트에서 로그인을 할 때 그 로그인한 상태를 유지할 수 있어야 하는데 이를 쿠키와 세션이 그 역할을 한다.
쿠키(Cookie)
클라이언트에 저장되는 키(key)와 값(value)이 들어있는 작은 데이터 파일
- 브라우저에서 로그인한 사용자 정보를 기억하게 하려면 쿠키에 그 사용자 정보를 저장하면 된다.
- 사용자가 로그인을 하면, 서버에서 id, password로 사용자를 식별한다.
- 그 다음, 쿠키를 생성하여 쿠키에 사용자 정보를 담아 클라이언트에 전달한다. (쿠키 저장소에 저장)
- 이후 브라우저(클라이언트)는 쿠키 저장소에서 해당 사용자 정보를 읽어 사용자 상태를 유지할 수 있다.
영속 쿠키, 세션 쿠키
영속 쿠키 : 쿠키에 만료 날짜를 생성 -> 해당 날짜까지 유지한다.
세션 쿠키 : 쿠키에 만료 날짜를 생략 -> 브라우저 종료 시까지만 유지한다.
- 보통 사용자 경험에 의해 브라우저 종료 시 로그아웃이 되길 기대하므로, 로그인 시, 세션 쿠키를 사용한다.
사용 예제 - 로그인
public String login(@Valid @ModelAttribute("loginForm")LoginForm form, BindingResult bindingResult,
HttpServletResponse response){
... form의 데이터를 가지고 사용자 조회 처리 ...
//사용자 조회 완료 시, 로그인 성공 처리
//쿠키를 만들어서 클라이언트에 전송
//세션 쿠키 : 쿠키에 시간 정보를 주지 않음. (브라우저 종료 시 모두 종료)
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));//String.valueOf : 값을 String으로 넘겨야 하기 때문에 사용.
response.addCookie(idCookie);
return "redirect:/";
}
- 쿠키 생성 시, 쿠키에 해당 사용자의 ID(식별 가능한 정보)를 저장 후, HttpServletResponse에 담아 클라이언트에 전송한다.
- 웹 브라우저는 종료 전까지 사용자의 id를 서버에 계속 보내줄 것이다. (세션 쿠키)
사용예제 - 로그아웃
//쿠키 만료시키는 메서드
private static void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0); // 유효시간 0
response.addCookie(cookie);
}
- 로그아웃의 경우, 쿠키의 유지시간을 만료시키면 된다.
@CookieValue
해당 어노테이션을 사용하면 클라이언트에서 넘어온 쿠키를 편리하게 조회할 수 있다.
@RequestMapping("/example")
public String example(@CookieValue(value = "myCookieName", defaultValue = "defaultCookieValue",
required = false) String myCookie) {
// 여기서 myCookie 변수는 "myCookieName" 이름의 쿠키 값을 포함하게 된다.
// 만약 해당 이름의 쿠키가 없다면 "defaultCookieValue"가 myCookie 변수에 할당된다.
}
- 만약 쿠키의 값이 필수적이고 해당 쿠키가 없을 경우 에러를 발생시키고 싶다면, required 속성을 true로 설정할 수 있다.
쿠키와 보안문제
1. 쿠키 값은 임의로 변경이 가능하다.
- 클라이언트가 쿠키를 강제로 변경하면 다른 사용자로 변경될 수 있다.
- Cookie: memberId=1 => Cookie: memberId=2 (다른 사용자의 이름이 보임)
2. 쿠키에 보관된 정보는 훔쳐갈 수 있다.
- 만약 쿠키에 개인 정보와 같이 중요한 정보들이 있으면, 이 정보가 웹 브라우저에도 보관되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다.
- 쿠키의 정보가 나의 로컬PC나 네트워크 전송 구간에서 탈취 당할 수 있다.
3. 해커가 쿠키를 한번 훔쳐 가면 평생 사용할 수 있다.
- 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 계속 시도할 수 있다.
대안
- 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출하고, 서버에서 토큰 과 사용자 id를 매핑해서 인식한다. 그리고 서버에서 토큰을 관리한다.
- 이때, 토큰은 예상 불가능하게 만들어야 한다.
- 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게(예: 30분) 유지 한다. 또는, 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 된다.
세션
서버에 중요한 정보를 보관하고 클라이언트와 서버를 추정불가능한 임의의 식별자 값으로 연결을 유지하는 방법
앞서 쿠키에 중요한 정보를 보관하는 방법은 보안적인 문제가 많았는데 이를 해결하려면 중요한 정보는 서버에 저장해야한다. 그리고 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결하여 사용자를 식별해야 한다.
로그인 - 세션 동작 방식
- 사용자가 loginId , password 정보를 전달하면 서버에서 해당 사용자가 맞는지 확인한다.
- 사용자 인증이 되면, 세션 ID를 생성한다.
- 세션 ID는 추정 불가능해야한다. => UUID로 생성
- UUID 예 : mySessionId=zz0101xx-bab9-4b92-9b32-dadb280f4b61
- 생성된 세션 ID와 세션에 보관할 값(memberA)을 서버의 세션 저장소에 보관한다.
- 세션 ID를 쿠키에 저장하여 클라이언트(쿠키 저장소)에 전송한다.
- 클라이언트와 서버는 쿠키로 연결이 되어야 한다.
- 서버는 클라이언트에 mySessionId라는 이름으로 세션ID만 쿠키에 담아서 전달한다.
- 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.
- 서버에서 클라이언트로 오직 추정 불가능한 세션 ID만 쿠키를 통해 전달하기 때문에 회원과 관련된 정보를 보내지 않는다.
- 로그인 이후, 클라이언트는 요청 시 항상 mySessionId 쿠키를 전달한다.
- 서버에서는 클라이언트가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션 정보를 사용한다.
세션을 통한 보안 대안
- 쿠키 값을 변조 가능 => 예상 불가능한 복잡한 세션 ID를 사용한다.
- 쿠키에 보관하는 정보는 클라이언트 해킹시 털릴 가능성이 있다. => 세션ID가 털려도 여기에는 중요한 정보를 저장하지 않기 때문에 안전하다.
- 쿠키 탈취 후 사용 => 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 세션의 만료시간을 짧게 (예: 30분) 유지한다. 또는 해킹이 의심되는 경우 서버에서 해당 세션을 강제로 제거하면 된다.
세션 관리
세션 관리는 크게 3가지 기능을 제공하면 된다.
- 세션 생성
- 세션 조회
- 세션 만료
코드로 작성하면 다음과 같다.
package hello.login.web.session;
import org.springframework.stereotype.Component;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* [세션 직접 만들기]
* 세션관리
* -세션 생성
* -sessionID 생성(임의의 추정불가능한 값)
* -세션 저장소에 sessionID와 보관할 값 저장
* -sessionID로 응답 쿠키를 생성해서 클라이언트에 전달
* -세션 조회
* -클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회
* -세션 만료
* -클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거
*/
@Component //스프링 빈 등록
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId"; //여러 곳에서 사용되기 때문에 상수로 지정
//동시에 여러 쓰레드가 접근하게 되면 동시성 문제가 발생하기 때문에 ConcurrentHashMap 사용.
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();//String->sessionID, Object->해당 값
/**
* -세션 생성
* * sessionID 생성(임의의 추정불가능한 값)
* * 세션 저장소에 sessionID와 보관할 값 저장
* * sessionID로 응답 쿠키를 생성해서 클라이언트에 전달
*/
public void createSession(Object value, HttpServletResponse response){
//sessionID 추정 불가능하게(UUID)생성하고 값을 세션에 저장
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
//쿠키 생성
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
}
/**
* -세션 조회
* * 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회
*/
public Object getSession(HttpServletRequest request){
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if(sessionCookie == null){
return null;
}
return sessionStore.get(sessionCookie.getValue()); //쿠키에 담긴 sessionID 값으로 세션 저장소의 값(value)을 찾음.
}
/**
* -세션 만료
* * 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거
*/
public void expire(HttpServletRequest request){
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
//쿠키에 담긴 sessionID 값으로 세션 저장소의 값(value)을 제거함.
if(sessionCookie != null){
sessionStore.remove(sessionCookie.getValue());
}
}
public Cookie findCookie(HttpServletRequest request, String cookieName){
//request에서 쿠키를 가져옴.
Cookie[] cookies = request.getCookies(); //쿠키는 배열로 넘어온다.
if(cookies == null){
return null;
}
//요청으로 넘어온 쿠키들 중 세션저장소의 쿠키 이름과 같은 것이 있으면, 그것의 value를 반환.
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny() //위의 조건을 만족하면, 그것의 값을 반환.(병렬적 처리)
.orElse(null);
}
}
세션을 활용한 로그인, 로그아웃 처리
로그인
public String loginV2(@Valid @ModelAttribute("loginForm")LoginForm form, BindingResult bindingResult,
HttpServletResponse response){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션관리자를 통해 세션을 생성하고, 회원 데이터를 세션에 보관.
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
로그아웃
public String logoutV2(HttpServletRequest request){
//세션을 만료
sessionManager.expire(request);
return "redirect:/";
}
'Spring > SpringMVC' 카테고리의 다른 글
서블릿 예외처리 (0) | 2024.02.15 |
---|---|
필터, 인터셉터 (0) | 2024.02.11 |
검증 - Bean Validation (0) | 2024.01.22 |
검증 - Validation 2 (1) | 2024.01.21 |
검증 - Validation (1) | 2024.01.15 |