본 프로젝트 자료는 김영한님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 참고 제작됐음을 알립니다.
1편에서 보안 문제가 왜 생기는지 알아보자.
이 보안 문제점을 해결해보도록 하자.
앞서 쿠키에 중요한 정보를 보관하는 방법은 여러가지 보안 이슈가 있었다. 이 문제를 해결하려면 결국 중요한 정보를 모두 서버에 저장해야 한다.
세션을 사용해서 보안을 강화하는 방법을 알아보기 전에 방식부터 배워보자.
사용자가 아이디 비번을 정보를 서버에 전달해 사용자 본인이 맞는지 조회해본다.
조회 결과 사용자가 본인이 맞다면 세션 임의의 ID 를 생성해준다.
클라이언트와 서버는 결국 쿠키로 연결 가능해야 한다.
여기서 중요한거는
서버가 정보를 넘길 때 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않아야 한다. 오직 세션ID 만 쿠키를 통해 전달해야 한다.
클라이언트의 세션id 쿠키 전달
세션을 사용해서 서버에서 중요한 정보를 관리하게 되었다. 덕분에 다음과 같은 보안 문제들을 해결할 수 있다.
세션을 랜덤으로 반환받는 로직을 만들기 전에 직접 세션을 만들어서 적용해보자.
세션 관리는 크게 3가지 기능을 제공한다.
이제 만들어보자.
SessionManager - 세션 관리
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);
}
}
SessionManagerTest - 테스트
위 클래스 내부 로직들이 정상 작동하는지 테스트 해보자.
여기서는
HttpServletRequest , HttpservletResponse
객체를 직접 사용할 수 없기 때문에 테스트에서 비슷한 역할을 해주는 가짜
MockHttpServletRequest , MockHttpServletResponse
를 사용했다.
문제 없이 잘 돌아가는걸 확인했다.
위에 만든 세션을 실제로 적용되는 확인해보자.
LoginController 클래스에서 로그인 로직 변경
LoginController 클래스에서 로그아웃 로직 변경
HomeController 클래스 homeLogin 변경
세션 관리자에서 저장된 회원 정보를 조회한다. 만약 회원 정보가 없으면, 쿠키나 세션이 없는 것 이므로 로그인 되지 않은 것으로 처리한다.
서블릿이 제공하는 HttpSession 도 결국 사용자가 직접 만든 SessionManager 와 같은 방식으로 동작한다. 쿠키 이름이 JSessionId 이고, 값도 마찬가지로 추정 불가능한 랜덤 값을 배정해준다.
이제 서블릿이 제공하는 HttpSession 을 사용하도록 개발해보자.
SessionConst 클래스 생성
HttpSession 에 데이터 보관 및 조회할 때 중복 방지를 위해 상수를 하나 정의.
loginV3 로직 수정
logoutV3 로직 수정
session.invalidate() : 로그아웃 시 생성된 세션을 제거한다.(재활용x)
HomeLoginV3 수정
request.getSession(false): 기본 홈 화면에서 로그인하지 않는 사용자들이 접속했을 때 의미 없는 세션 생성 방지를 위해 false.
session.getAttribute(SessionConst.LOGIN_MEMBER): 만약 로그인 한 회원이 있다면 다시 로그인하는걸 방지하기 위해 세션 보관함에 회원 객체를 찾아 반영한다.
스프링은 세션을 더욱 편리하게 사용할 수 있도록 사용자에게 제공해준다.
homeLoginV3 수정
세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 한번에 편리하게 처리해주는 것을 확인할 수 있다.
그리고 로그인 성공했을 때 마다 url 창에 JSession 이 포함되어 사용자에게 출력이 되는데,
이것은 웹 브라우저가 쿠키를 지원받지 못해 쿠키 대신 사용자 url 을 통해 세션을 유지하기 위해 보여지는 모습이다. 이것을 해결 하기 위해서는 다음 옵션을 넣어주면 간단하게 해결 할 수 있다.
application.properties
application.properties 에 다음과 같이 입력해주면 된다.
url 에 세션이 사라진 모습을 확인 할 수 있다.
로그인 이후 사용자가 1분 동안 아무것도 하지 않았을 때 자동적으로 로그아웃 처리 해주는 로직 관련해서 배워볼려고 한다.
기존 코드에 새로운 클래스 하나만 추가해주면 이 로직을 사용할 수 있다.
SessionInfoController 신규 클래스 생성
세션 타임아웃 설정은 대부분 사용자가 로그아웃을 하지 않고 홈페이지를 종료한다. 이때 Http가 비연결성으로 해당 사용자가 홈페이지를 사용을 그만뒀다고 판단하기 힘들어 계속 로그인 상태를 유지하게 된다. 따라서 서버에서 세션 데이터를 언제 삭제해야할지 판단하기 어렵다.
이를 대비하기 위해 세션 타임아웃 설정을 만들어둔다.
추가적으로 해야할 설정
application.properties
application.properties 에 사용자가 설정된 시간내에 사용하지 않을 경우 로그아웃할 수 있게 시간을 설정 해줘야 한다.
테스트를 위해 60초로 짧게 설정해본다.
이렇게 하고 실행해보면 로그인 시점으로 정확히 60초 뒤에 홈페이지를 사용해 보면 자동으로 로그아웃이 된 거를 확인할 수 있다.