[Spring] 쿠키의 보안 문제와 세션
섹션 6. 로그인 처리1 - 쿠키, 세션
2023.03.15 - [Spring] - [Spring] 로그인, 로그아웃 처리 - 쿠키, 세션
지난 포스트에서 쿠키를 사용하여 로그인을 유지할 수 있었다. 여기에는 심각한 보안 문제가 있다.
쿠키의 보안 문제
쿠키 값은 변경이 가능하다.
로그인 후 개발자 도구를 켜서 Application 탭을 클릭하자. 그다음 Storage>Cookies를 클릭하면 쿠키를 확인할 수 있다. 여기서 Value 값을 클릭하면 임의로 변경이 가능하다.
또한 쿠키에는 다양한 정보를 담을 수 있는데 여기에는 중요한 개인정보나 금융 관련 정보도 있을 수 있다. 이 정보는 웹 브라우저에 저장되고 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다. 따라서 PC나 네트워크 전송 구간에서 쿠키가 훔쳐질 수 있다. 이렇게 훔쳐진 쿠키는 평생 사용될 수 있다.
해결하기 위한 대안은 아래와 같다.
- 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측이 불가능한 임의의 토큰(랜덤 값)을 노출하고, 서버에서 노큰과 사용자 Id를 매핑해서 인식한다. 서버에서 토큰을 관리한다.
- 토큰의 값은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상이 불가능해야 한다.
- 해커가 토큰을 훔쳐도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다. 또는 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거한다.
세션
위에서 쿠키에는 다양한 정보가 담길 수 있으며 여기에는 중요한 정보도 포함될 수 있다고 언급했다. 이러한 문제를 해결하기 위해 중요한 정보는 모두 서버에 저장해야 하며, 클라이언트와 서버는 예측이 불가능한 값을 통해 연결되어야 한다.
위와 같이 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라고 한다. 세션은 쿠키를 기반으로 하지만, 쿠키와 달리 정보를 웹 브라우저에 저장하지 않고 서버에서 관리한다.
세션을 이용하여 로그인 처리를 해보자.
로그인
세션을 직접 개발할 수 있지만, WAS(tomcat)에서 생성하는 HttpServletRequest와 HttpServletResponse로 세션을 쉽게 관리할 수 있다.
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm loginForm,
BindingResult bindingResult,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
//글로벌 오류
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
로그인 성공 처리 로직을 보자.
1. HttpServletRequest의 getSession()으로 세션을 가져온다. 이때 세션이 있으면 해당 세션을 반환하고, 없으면 새로운 세션을 생성한다. (default값은 true로 세션이 없다면 신규 세션을 생성하고, 새로운 세션을 생성하고 싶지 않다면 false를 넣는다)
2. setAttribute()를 사용하여 가져온 세션에 로그인한 회원의 정보를 보관한다. 첫 번째 파라미터는 바인딩된 객체의 이름(String), 두 번째 파라미터는 바인딩할 객체(Object)다. 위 코드는 직접 작성한 상수로 선언한 값과 로그인한 객체를 담았다.
이미 로그인된 사용자를 찾을 때는 스프링에서 제공하는 @SessionAttribute를 사용하면 된다. 아래 코드는 세션을 찾고, 세션에 담긴 데이터를 찾는 과정을 스프링이 처리하는 것을 보여준다.
@GetMapping("/")
public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member member,
Model model) {
//세션에 회원 데이터가 없으면 home
if (member == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", member);
return "loginHome";
}
name은 바인딩할 세션의 이름이고, required는 세션이 필수인지 여부를 나타낸다. request.getSession()에서 false나 true값을 넘기는 것과 동일하다. 위에서는 false를 주었으므로 세션이 없어도 새로운 세션을 생성하지 않는다.
로그아웃
로그아웃도 로그인과 마친가지로 HttpServletRequest를 사용하면 된다.
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
//세션을 가져오고, 없어도 새로 만들지 않도록 false로 넘긴다.
HttpSession session = request.getSession(false);
if(session != null) {
session.invalidate();
}
return "redirect:/";
}
로그인과 다른 점은 getSession()에 false를 넘기는 것이다. 이는 로그아웃이므로 세션이 없을 때 새로 만들지 않기 위해서이다.
세션을 가져오고 세션이 null이 아니라면 invalidate()를 사용하여 세션을 제거한다.