1. 쿠키 사용
로그인 상태를 유지하기 위해서는 쿠키를 사용할 수 있다. 서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아서 전달하고, 브라우저는 앞으로 해당 쿠키를 지속적으로 보내면 된다. 쿠키의 종류는 다음과 같다.
- 영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지된다.
- 세션 쿠키: 만료 날짜를 생략하면 브라우저 종류시까지만 유지된다.
우리는 세션 쿠키를 사용해볼 것이다. 쿠키는 다음과 같이 response에 담을 수 있다.
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletResponse response) {
...
Cookie cookieId = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(cookieId);
...
}
이후 로그인을 시도해보면 다음과 같이 쿠키가 잘 전달된 것을 볼 수 있다.
쿠키 삭제는 다음과 같이 기존 쿠키를 null로 설정하고, 지속 시간은 0초로 줌으로써 만료시킬 수 있다.
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
exppireCookie(response, "memberId");
return "redirect:/";
}
private void exppireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
1.1. 쿠키의 보안 문제
하지만 이런 쿠키는 다음과 같은 보안 문제가 있다.
- 쿠키 값은 임의로 변경가능하다.
- 쿠키에 보관된 정보는 훔쳐갈 수 있다. (개인정보, 신용카드 정보가 있다면 심각한 문제가 된다.)
- 해커가 쿠키를 한 번 훔쳐가면 평생 사용할 수 있다.
이를 해결하기 위해 다음과 같은 대안이 있다.
- 쿠키에 중요한 값을 노출하면 안된다.
- 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출해야 한다. 그리고 이 토큰을 사용자 id에 매핑해야 한다.
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상할 수 없어야 한다.
- 해커가 토큰을 가져가도 시간이 지나면 사용할 수 없게 해야한다.
2. 세션 사용
쿠키에는 여러 보안적 이슈가 있고 이런 문제를 해결하려면 결국 서버에 중요한 정보를 저장해야 한다. 그리고 클라이언트와 서버 사이에서는 임의의 식별자인 토큰(랜덤값)을 통해 통신해야 한다. 이렇게 서버에 중요한 정보를 저장하고 연결을 유지하는 방법을 세션이라고 한다. 세션 동작 방식은 다음과 같다.
- 세션 아이디를 만들 때는 UUID를 사용한다.
- 쿠키로는 세션 아이디를 전달한다.
UUID의 기본적인 개념은 다음과 같다.
- UUID는 'Universally Unique Identifier'의 약자로 128-bit의 고유 식별자다.
- UUID는 중앙 시스템에 등록하고 발급하는 과정이 없어서 상대적으로 더 빠르고 간단하다.
- 단, 완전히 고유하지 않을 확률이 있긴 하지만 발생할 확률이 10억분의 1로 굉장히 작다.
자바에서 UUID를 사용 방법은 다음과 같다.
import java.util.UUID;
UUID.randomUUID().toString()
2.1. 세션 직접 만들어 보기
세션은 3가지 기능을 제공한다.
- 세션 생성: sessionId를 생성하고, 세션 저장소에 sessionId와 보관할 값을 저장한다. 이후 sessionId로 쿠키를 생성해 클라이언트에 전달한다.
- 세션 조회: 클라이언트가 요청한 sessionId 쿠키 값으로 세션 저장소에 보관한 값을 조회한다.
- 세션 만료: 클라이언트가 요청한 sessionId 쿠키 값으로 세션 저장소에 보관한 sessionId와 값을 제거한다.
먼저 세션을 관리하기 위해 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 cookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(cookie);
}
...
}
- UUID를 생성하고 전달된 value(Member 객체)와 함께 store에 넣는다.
위에서 세션을 저장했으므로 조회할 수 있어야 한다. 해당 코드는 다음과 같다.
public Object getSession(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie == null) {
return null;
}
return sessionStore.get(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);
}
- findCookie(): 전달된 쿠키 중 우리가 전달했던 이름의 쿠키가 있는지 찾고, 해당 쿠키를 반환해준다.
- getSession(): findCookie를 통해 반환된 쿠키의 value(UUID)를 통해 저장된 Member객체를 반환한다.
만료 기능 코드는 다음과 같다.
public void expire(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie != null) {
sessionStore.remove(sessionCookie.getValue());
}
}
- expire(): 요청된 쿠키 값을 store에서 제거한다.
이후 로그인에 적용해보면 쿠키가 잘 전달되는 것을 확인할 수 있다.
2.2. 서블릿의 HTTP Session
서블릿이 제공하는 HttpSession도 위에서 만든 SessionManager와 동일한 방식으로 동작한다. 로그인 기존의 SessionManager를 사용하던 Login 코드는 다음과 같다.
public String login(@Valid @ModelAttribute 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";
}
log.info("user {} login", loginMember.getLoginId());
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
위 코드에서 sessionManager를 사용한 부분은 아래와 같이 작성된다.
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
...
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
...
}
- request.getSession(true): 세션이 있는 경우 기존 세션을 반환하고 세션이 없는 경우 새로운 세션을 생성한다. (default)
- request.getSession(false): 세션이 있는 경우 기존 세션을 반환하고 없는 경우 null을 반환한다.
해당 세션의 아이디는 서블릿에서 JSessionId라는 쿠키로 전달해준다.
세션 만료의 경우 아래와 같이 코드를 작성하면 된다.
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
- session.invalidate(): session의 모든 쿠키 및 데이터를 제거한다.
로그인을 처리하는 HomeController는 다음과 같이 @SessionAttribute를 사용하면 쉽게 작성할 수 있다.
@GetMapping("/")
public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member member, Model model) {
if (member == null) {
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
- @SessionAttribute를 사용하면 좀 더 쉽게 세션을 사용할 수 있다.
그런데 로그인 이후 url을 확인해보면 JSessionId가 URL에 포함된 것을 볼 수 있다.
만약 웹 브라우저가 쿠키를 지원하지 않으면 쿠키 대신 URL을 통해 세션을 유지하기 위한 방법이다. 하지만 이 방법은 모든 URL에 해당 id값을 포함해야하기 때문에 거의 사용하지 않는다. 만약 URL 전달 방식을 끄고 싶다면 아래와 같은 설정을 application.properties에 추가하면 된다.
server.servlet.session.tracking-modes=cookie
2.3. 세션 정보와 타임아웃
먼저 세션의 정보를 확인하기 위해 다음과 같이 코드를 작성하고 로그를 확인해보자.
@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("creationTime={}", new Date(session.getCreationTime()));
log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));
log.info("isNew={}", session.isNew());
return "세션 출력";
}
}
- sessionId: JSessionId와 같은 값이다.
- maxInactiveInterval: 세션의 유효 시간을 말한다.
- creationTime: 생성된 시간을 의미한다.
- lastAccessedTime: 마지막으로 접근한 시간을 가져올 수 있는 메서드다.
- 타임아웃은 마지막 접근 시간 기중 유효 시간이 경과하면 발생한다.
출력 결과는 다음과 같다.
timeout 시간은 application.properties에 다음 내용을 작성하면 설정할 수 있다. (단위는 초 단위이다.)
server.servlet.session.timeout = 1800
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'BackEnd > Spring' 카테고리의 다른 글
[Spring MVC-2] 예외 처리 (0) | 2024.06.27 |
---|---|
[Spring MVC-2] 로그인 - 필터, 인터셉터 (0) | 2024.05.27 |
[Spring MVC-2] Bean Validation (0) | 2024.05.24 |
[Spring MVC-2] Validation (0) | 2024.05.23 |
[Spring DB-1] 문제해결 - 예외 처리, 반복 (0) | 2024.05.08 |