Spring MVC - 로그인 처리(쿠키, 세션)
어떤 웹 사이트를 방문하여 어떤 작업을 하는데에 있어서 로그인을 하는것은 거의 필수적이다. 스프링은 다양한 방식으로 로그인 처리를 할 수 있는데 한번 살펴보자.
쿠키, 세션을 사용하여 로그인 처리를 해볼 건데 말로 설명하면 대략 이런 순서대로 동작하는 로직이 될 것이다.
- 사용자가 로그인 화면에 접속하여 자신의 아이디와 비밀번호를 입력한다.
- 아이디와 비밀번호가 맞지 않는다면 경고 메시지를 출력하며 다시 입력하라고 한다.
- 아이디가 맞다면 다음으로 넘어간다.
- 아이디에 해당하는 회원정보를 데이터베이스에서 가져온다.
- 화면이 로그인된 상태의 화면으로 바뀐다.
하지만 마지막 과정인 3번은 쿠키와 세션을 사용하지 않는다면 동작하지 않을것이다. 왜냐하면 웹 페이지는 정적인 페이지이기 때문에 웹 서버에 쿠키를 전달해야 3번 과정이 가능하기 때문이다.
쿠키와 보안 문제
단순히 쿠키만을 사용하여 로그인ID를 웹 서버에 전달하여 로그인을 유지할 수 있다. 하지만 이런 방식의 로그인 유지 방법은 심각한 보안문제를 야기한다. 내 정보가 해커한테 다 털려갈 수 있다...
- 쿠키 값은 임의로 변경할 수 있다.
- 크롬 웹 브라우저 -> 검사 -> Application -> Cookie 에서 값을 변경하면 해당하는 쿠키 값의 사용자로 로그인이 된다!
- 즉, 클라이언트가 쿠키 값을 변경하면 다른 사용자로 로그인을 할 수 있다.
- 굉장히 어려운 쿠키 값을 생성하여 전달해야함!
- 쿠키에 보관된 정보는 훔쳐갈 수 있다.
- 위에 설명했던 것 처럼 쿠키 값은 크롬 웹 브라우저에서 클라이언트가 확인이 가능하다.
- 따라서 쿠키 값을 누구든지 확인할 수있기 때문에 훔쳐갈 수 있다.
대안
- 쿠키에 중요한 값(개인정보 등)을 노출하지 않고, 임의의 랜덤 값을 웹 서버에 전달한다.
- 해커가 어찌해서 쿠키 값을 알아내도 시간이 지나면 사용할 수 없도록 서버에서 해당 쿠키의 지속시간을 짧게 유지한다.
위의 적은 두 대안은 UUID 값을 사용하는 것과 세션을 사용하는 것이다. UUID는 추정이 불가능한 임의의 토큰 값이기 때문에 많이 사용한다. UUID를 세션 ID로 할당하고 세션에 보관할 값은 회원에 대한 정보를 넣는 것이다.
다음 그림은 내가 그린 쿠키와 세션의 동작원리이다. 쿠키와 세션을 동시에 쓴 이유는 세션매니저라는 객체를 직접 생성해서 사용할 것이기 때문에 브라우저에 세션 ID 정보와 함께 쿠키를 전달하여 유지하고, 로그아웃시에 쿠키에 저장된 세션 ID를 가지고와서 세션을 삭제할 것이기 때문이다. 결국 하고싶은 말은 서블릿을 사용하면 쿠키와 세션 둘중에 하나만 사용해도 된다.
위의 내가 그린 쿠키와 세션의 동작원리에 따라서 로그인 처리를 적용해볼 것이다. 일단 처음에는 순수한 자바코드를 많이 쓰고 점점 스프링과 서블릿을 쓰면서 코드를 작성해보겠다.
Version 1
버전 1은 세션관리를 순수 자바로 작성할 것이기 때문에 세션을 생성, 조회, 삭제하는 코드를 짜보았다. 세션을 보관하는 곳은 싱글톤으로 생성해야 하기 때문에 스프링 컨테이너에 등록도 해두었다.
@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
/**
* 세션 생성
* sessionId 생성 (임의의 추정 불가능한 랜덤 값)
* 세션 저장소에 sessionId와 보관할 값 저장
* sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
*/
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);
}
}
다음은 컨트롤러 부분이다.
@PostMapping("/login")
public String login(@Validated @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";
}
//로그인 성공 처리 -> 쿠키 처리
//세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
위 두 코드를 해석하면 다음과 같다. 사용자가 아이디와 비밀번호를 입력해서 서버로 HTTP 요청을 보낸다. 서버는 사용자가 입력한 로그인 입력 폼을 토대로 검증을 수행하고, 검증 오류가 발생하지 않으면 데이터베이스에서 회원 정보를 찾아온다. 찾아온 회원 정보를 토대로 세션에 회원 데이터를 보관하고, 이를 쿠키에 넣어서 웹 브라우저에 쿠키를 전달한다. 사용자는 이제 로그인된 화면을 볼 수 있다.
로그아웃은 다음과 같은 방식으로 세션을 보관하는 객체에서 해당 세션을 삭제하는 방식으로 처리하면 된다.
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request){
sessionManager.expire(request);
return "redirect:/";
}
Version 2
이번에는 순수 자바를 사용하는 것 보다는 서블릿을 사용해서 로그인 처리를 해볼 것이다. 다음 코드를 보면 서블릿을 사용하면 보다 쉽게 쿠키와 세션을 처리할 수 있는 것을 볼 수 있다. 서블릿을 사용할 때는 세션만을 사용해서 로그인처리를 해볼 것이다. 쿠키를 사용하는 것이 응답속도가 빠르다는 장점이 있지만, 세션을 사용한다면 보안적인 측면에서 뛰어나기 때문이다.
@PostMapping("/login")
public String loginV3(@Validated @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request){
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리 -> 쿠키 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
서블릿을 사용하면 요청정보에서 세션정보를 꺼내올 수 있고, 세션을 생성해서 사용자 정보를 넘겨줄 수 있다.
- request.getSession(@Nullable boolean create): HTTP 요청정보에서 세션정보를 꺼낸다. 만약 인자로 true를 넣거나 아무것도 넣지 않는다면 세션정보가 없어도 새로 만들어서 반환해준다. false를 인자로 넣으면 세션정보가 없을 때, 세션을 반환하지 않는다.
- session.setAttribute(String name, Object value): 세션의 이름과 세션에 담을 정보를 넣는다.
서블릿을 사용하여 로그아웃하는 방법은 다음과 같다.
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request){
HttpSession session = request.getSession(false);
//세션이 존재하면 세션을 제거
if(session != null){
session.invalidate();
}
return "redirect:/";
}
여기서 주의할점은 세션정보를 불러올 때 세션을 새로 생성하면 안된다는 것이다. 반드시 인자에 false를 넣도록 하자!
- session.invalidate(): 현재 세션을 무효화한다.
- session.removeAttribute(String name): 특정 세션을 지운다.
'Spring Framework > Spring Web MVC' 카테고리의 다른 글
Spring MVC - FrontController(Dispatcher Servlet) (0) | 2022.04.09 |
---|---|
Spring MVC - API 예외처리(Exception) (0) | 2022.04.05 |
Spring MVC - Bean Validation (0) | 2022.03.29 |
Spring MVC - Validation(검증) (0) | 2022.03.28 |
HTTP 메시지 컨버터(HTTP Message Converter) (0) | 2022.03.27 |