[Spring] 로그인 및 회원가입 제작(2)

JJoSuk·2023년 6월 9일
0
post-thumbnail

본 프로젝트 자료는 김영한님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 참고 제작됐음을 알립니다.

쿠키와 보안 문제

1편에서 보안 문제가 왜 생기는지 알아보자.

보안 문제

쿠키 값은 임의로 변경할 수 있다.

  • 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.
  • 실제 웹브라우저 개발자모드 -> Application -> Cookie 변경으로 확인
  • Cookie: memberId=1 -> Cookie: memberId=2 (다른 사용자의 이름이 보임)

쿠키에 보관된 정보는 훔쳐갈 수 있다.

  • 만약 쿠키에 개인정보나, 신용카드 정보가 있다면?
  • 이 정보가 웹 브라우저에도 보관되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다.
  • 쿠키의 정보가 나의 로컬 PC에서 털릴 수도 있고, 네트워크 전송 구간에서 털릴 수도 있다.

해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.

  • 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 계속 시도할 수 있다.

대안

  • 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출하고, 서버에서 토큰과 사용자 id를 매핑해서 인식한다. 그리고 서버에서 토큰을 관리한다.
  • 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
  • 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게(예: 30분) 유지한다. 또는 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 된다.

이 보안 문제점을 해결해보도록 하자.


로그인 처리하기 - 세션 동작 방식

앞서 쿠키에 중요한 정보를 보관하는 방법은 여러가지 보안 이슈가 있었다. 이 문제를 해결하려면 결국 중요한 정보를 모두 서버에 저장해야 한다.

  • 그리고 클라이언트와 서버는 추정 불가능한 임의의 식별자 값으로 연결해야 한다.
  • 이렇게 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라 한다.

세션을 사용해서 보안을 강화하는 방법을 알아보기 전에 방식부터 배워보자.

세션 동작 방식

  1. 로그인

사용자가 아이디 비번을 정보를 서버에 전달해 사용자 본인이 맞는지 조회해본다.

  1. 세선 생성

조회 결과 사용자가 본인이 맞다면 세션 임의의 ID 를 생성해준다.

  • 생성해줄 때 랜덤으로 추정 불가능한 값으로 반환해줘야 한다.

  1. 세션ID 응답 쿠키로 전달

클라이언트와 서버는 결국 쿠키로 연결 가능해야 한다.

  • 서버는 서버가 생성한 세션ID 만 쿠키에 담아서 전달한다.
  • 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.

여기서 중요한거는

서버가 정보를 넘길 때 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않아야 한다. 오직 세션ID 만 쿠키를 통해 전달해야 한다.

  1. 클라이언트의 세션id 쿠키 전달

    1. 전달 받은 클라이언트는 해당 사이트에서 동작할 때 마다 mySessionId 쿠키만 전달한다.
    2. 전달 받은 서버는 mySessionId 쿠키 정보로 세션 저장소를 조회해서 사용자 정보를 사용한다.

정리

세션을 사용해서 서버에서 중요한 정보를 관리하게 되었다. 덕분에 다음과 같은 보안 문제들을 해결할 수 있다.

  • 쿠키 값을 변조 가능 -> 예상 불가능한 복잡한 세션Id를 사용한다.
  • 쿠키에 보관하는 정보는 클라이언트 해킹시 털릴 가능성이 있다 -> 세션Id가 털려도 여기에는 중요한 정보가 없다.
  • 쿠키 탈취 후 사용 -> 해커가 토큰을 털어가도 시간이 지나면 사용 할 수 없도록 서버에서 세션의 만료시간을 짧게(예: 30분) 유지한다.
  • 해킹이 의심되는 경우 서버에서 해당 세션을 강제로 제거하면 된다.

로그인 처리하기 - 세션 직접 만들기

세션을 랜덤으로 반환받는 로직을 만들기 전에 직접 세션을 만들어서 적용해보자.

세션 관리는 크게 3가지 기능을 제공한다.

1. 세션 생성

  • sessionId 생성 (임의의 추정 불가능한 랜덤 값)
  • 세션 저장소에 sessionId와 보관할 값 저장
  • sessionId로 응답 쿠키를 생성해서 클라이언트에 전달

2. 세션 조회

  • 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회

3. 세션 만료

  • 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 sessionId와 값 제거

이제 만들어보자.


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);
    }
}

로직은 어렵지 않아서 쉽게 이해가 될 것이다.

  • @Component : 스프링 빈으로 자동 등록한다.
  • ConcurrentHashMap : HashMap 은 동시 요청에 안전하지 않다.
  • 동시 요청에 안전한 ConcurrentHashMap 를 사용했다.

SessionManagerTest - 테스트

위 클래스 내부 로직들이 정상 작동하는지 테스트 해보자.

여기서는
HttpServletRequest , HttpservletResponse
객체를 직접 사용할 수 없기 때문에 테스트에서 비슷한 역할을 해주는 가짜
MockHttpServletRequest , MockHttpServletResponse
를 사용했다.

문제 없이 잘 돌아가는걸 확인했다.


로그인 처리하기 - 직접 만든 세션 적용

위에 만든 세션을 실제로 적용되는 확인해보자.

LoginController 클래스에서 로그인 로직 변경

기존 코드에서 바뀐 내용

  • private final SessionManager sessionManager; 주입
    sessionManager.createSession(loginMember, response);
  • 로그인 성공시 세션을 등록한다. 세션에 loginMember 를 저장해두고, 쿠키도 함께 발행한다.

LoginController 클래스에서 로그아웃 로직 변경

기존 코드에서 바뀐 내용

  • 기존 logout() 의 @PostMapping("/logout") 주석 처리
    로그아웃 시 해당 세션의 정보를 제거한다.

HomeController 클래스 homeLogin 변경

기존 코드에서 바뀐 내용

  • private final SessionManager sessionManager;
  • 주입 기존 homeLogin() 의 @GetMapping("/") 주석 처리

세션 관리자에서 저장된 회원 정보를 조회한다. 만약 회원 정보가 없으면, 쿠키나 세션이 없는 것 이므로 로그인 되지 않은 것으로 처리한다.


로그인 처리하기 - 서블릿 HTTP 세션1

HttpSession 소개

서블릿이 제공하는 HttpSession 도 결국 사용자가 직접 만든 SessionManager 와 같은 방식으로 동작한다. 쿠키 이름이 JSessionId 이고, 값도 마찬가지로 추정 불가능한 랜덤 값을 배정해준다.

이제 서블릿이 제공하는 HttpSession 을 사용하도록 개발해보자.

SessionConst 클래스 생성

HttpSession 에 데이터 보관 및 조회할 때 중복 방지를 위해 상수를 하나 정의.

loginV3 로직 수정

  • 세션을 생성하려면 request.getSession() 을 사용하면 된다.
    • request.getSession() 에 true 와 false 2가지 create 옵션이 있다.
    • request.getSession(true)
      • 기본적으로 true 를 생략할 경우 디폴드 값으로 true 가 생성된다.
      • 세션이 있으면 기존 세션을 반환한다.
      • 세션이 없으면 새로운 세션을 생성해서 반환한다.
    • request.getSession(false)
      • 세션이 있으면 기존 세션을 반환한다.
      • 세션이 없으면 새로운 세션을 생성하지 않고 null 을 반환한다.

logoutV3 로직 수정

session.invalidate() : 로그아웃 시 생성된 세션을 제거한다.(재활용x)

HomeLoginV3 수정

  • request.getSession(false): 기본 홈 화면에서 로그인하지 않는 사용자들이 접속했을 때 의미 없는 세션 생성 방지를 위해 false.

  • session.getAttribute(SessionConst.LOGIN_MEMBER): 만약 로그인 한 회원이 있다면 다시 로그인하는걸 방지하기 위해 세션 보관함에 회원 객체를 찾아 반영한다.


로그인 처리하기 - 서블릿 HTTP 세션2

@SessionAttribute

스프링은 세션을 더욱 편리하게 사용할 수 있도록 사용자에게 제공해준다.

homeLoginV3 수정

세션을 찾고, 세션에 들어있는 데이터를 찾는 번거로운 과정을 스프링이 한번에 편리하게 처리해주는 것을 확인할 수 있다.

그리고 로그인 성공했을 때 마다 url 창에 JSession 이 포함되어 사용자에게 출력이 되는데,

이것은 웹 브라우저가 쿠키를 지원받지 못해 쿠키 대신 사용자 url 을 통해 세션을 유지하기 위해 보여지는 모습이다. 이것을 해결 하기 위해서는 다음 옵션을 넣어주면 간단하게 해결 할 수 있다.

application.properties

application.properties 에 다음과 같이 입력해주면 된다.

url 에 세션이 사라진 모습을 확인 할 수 있다.


세션 정보와 타임아웃 설정

로그인 이후 사용자가 1분 동안 아무것도 하지 않았을 때 자동적으로 로그아웃 처리 해주는 로직 관련해서 배워볼려고 한다.

기존 코드에 새로운 클래스 하나만 추가해주면 이 로직을 사용할 수 있다.

SessionInfoController 신규 클래스 생성

  • sessionId : 세션Id, 로그인 했을 때 세션 보관함에 저장된 JSESSIONID 의 값이다. 예) 34B14F008AA3527C9F8ED620EFD7A4E1
  • maxInactiveInterval : 세션의 유효 시간 설정할 수 있다. 예) 60초
  • creationTime : 세션 생성일시, 로그인 했을 때 부터 확인하기 위해 세션 생성 시점 시간.
  • lastAccessedTime :세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.
  • isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로 sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부

세션 타임아웃 설정

세션 타임아웃 설정은 대부분 사용자가 로그아웃을 하지 않고 홈페이지를 종료한다. 이때 Http가 비연결성으로 해당 사용자가 홈페이지를 사용을 그만뒀다고 판단하기 힘들어 계속 로그인 상태를 유지하게 된다. 따라서 서버에서 세션 데이터를 언제 삭제해야할지 판단하기 어렵다.

이를 대비하기 위해 세션 타임아웃 설정을 만들어둔다.

  • 세션 타임아웃 설정을 했을 때 장점
    • 사용자가 로그아웃하지 않아도 일정 시간 사용하지 않으면 자동 로그아웃이 되어 개인정보 보호에 도움된다.
    • 이 로직을 사용하지 않았을 경우 많은 사용자가 로그아웃을 하지 않고 장시간 사용할 경우 세션 보관함에 사용자 세션이 계속해서 요청이 들어와 서버에 무리가 올 수 있다.

추가적으로 해야할 설정

application.properties

application.properties 에 사용자가 설정된 시간내에 사용하지 않을 경우 로그아웃할 수 있게 시간을 설정 해줘야 한다.

테스트를 위해 60초로 짧게 설정해본다.

이렇게 하고 실행해보면 로그인 시점으로 정확히 60초 뒤에 홈페이지를 사용해 보면 자동으로 로그아웃이 된 거를 확인할 수 있다.

정리

  • 서블릿의 HttpSession 이 제공하는 타임아웃 기능 덕분에 세션을 안전하고 편리하게 사용할 수 있다.
  • 실무에서 주의할 점은 세션에는 최소한의 데이터만 보관해야 한다는 점이다.
  • 보관한 데이터 용량 * 사용자 수로 세션의 메모리 사용량이 급격하게 늘어나서 장애로 이어질 수 있다.
  • 추가로 세션의 시간을 너무 길게 가져가면 메모리 사용이 계속 누적 될 수 있으므로 적당한 시간을 선택하는 것이 필요하다.
  • 기본이 30 분이라는 것을 기준으로 고민하면 된다.
profile
안녕하세요

0개의 댓글