티스토리 뷰

로그인 상태 유지하기

웹 브라우저랑 서버 사이에서 로그인 했다는 것이 유지가 되어야한다.

쿼리 파라미터를 계속 유지하는 것은 어렵고 비효율적이므로 쿠키를 사용한다.

 

쿠키

서버에서 로그인에 성공하면 http 응답에 쿠키를 담아서 브라우저에 전달한다. 그러면 브라우저는 앞으로 해당 쿠키를 지속해서 보내준다.

 

쿠키에는 영속 쿠키와 세션 쿠키가 있다

  • 영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지
  • 세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지

브라우저 종료시 로그아웃이 되길 기대하므로, 우리에게 필요한 것은 세션 쿠키이다.

 

 

쿠키와 보안문제

  • 쿠키의 값이 임의로 변경될 수 있다
    •  이런식으로 개발해버리면 사용자 모두가 털릴 수 있다 !
    • 실제 개발자모드에서 쿠키를 마음대로 바꿀 수 있다
  • 쿠키에 보관된 정보는 훔쳐갈 수 있다
    • 만약 쿠키에 개인정보나, 신용카드 정보가 있다면..?
    • 이 정보가 웹 브라우저에도 보관되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다.
  • 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다
    • 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 시도할 수 있다.
  • 대안
    • 쿠키에 중요한 값을 노출하지 않고, 사용자별로 예측 불가능한 임의의 토큰 값을 노출하고, 서버에서 토큰과 사용자 id를 매핑해서 인식한다. 그리고 서버에서 토큰을 관리한다 (예를 들어 JWT 토큰)
    • 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
    • 토큰 만료 시간을 짧게(30분) 유지한다.

세션

서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라 한다.

  • 쿠키 값을 변조 가능 -> 예상 불가능한 복잡한 세션 ID를 사용한다. (외부에서 접근 불가)
  • 쿠키에 보관하는 정보는 클라이언트 해킹시 털릴 가능성이 높다 -> 세션 ID가 털려도 여기에는 중요한 정보가 없다
  • 쿠키 탈취 후 사용 -> 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 세션의 만료 시간을 짧게 유지한다.

 

먼저 세션을 간단하게 직접 구현해 보겠습니다.

 

createSession 함수를 보면, 자바 UUID를 통해 사용자의 세션 아이디를 랜덤으로 생성합니다.

첫 번째 인자인 value에는 로그인 하려는 Member 객체가 전달됩니다. 해당 객체와 세션 아이디의 조합으로 sessionStore에 저장한 후 쿠키도 함께 생성합니다. 마지막에는 반드시 response에 쿠키를 담아야 합니다.

나머지 함수들 역시 로직은 간단하므로 쉽게 이해할 수 있을 것입니다 !

public class SessionManager {

    public static final String SESSION_COOKIE_NAME = "mySessionId";
    private Map<String, Object> sessionStore = new ConcurrentHashMap<>();

    /**
     * 세션 생성
     * 세션 아이디 생성 (임의의 추정 불가능한 랜덤 값)
     * 세션 저장소에 세션 아이디와 보관할 값 (ID, Value) 저장
     * 세션 아이디로 응답 쿠키를 생성해서 클라이언트에 전달
     */
    public void createSession(Object value, HttpServletResponse response){

        // 세션 아이디 생성하고 값을 세션에 저장
        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){
        Cookie[] cookies = request.getCookies();
        if (cookies == null){
            return null;
        }

        return Arrays.stream(cookies)
                .filter(cookie -> cookie.getName().equals(cookieName))
                .findFirst()
                .orElse(null);
    }
}

 

서블릿은 세션을 위해 HttpSession이라는 기능을 제공하는데 앞에서 직접 구현한 세션의 개념이 이미 구현되어 있는 기능입니다.

서블릿을 통해 HttpSession을 생성하면 쿠키 이름이 JSESSIONID이고, 값은 UUID와 마찬가지로 추정 불가능한 랜덤값입니다.

간단하게 사용되는 예제를 적어놓겠습니다.

// 세션을 조회해서 세션이 있으면 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();

// 세션에 로그인 회원 정보 보관
session.setAttribute("Login Member", loginMember);

세션을 생성하려면 request.getSession 함수 인자로 true를 사용하면 됩니다.

즉 세션을 조회해서 세션이 존재하지 않는 경우 세션을 생성한다는 뜻입니다. 함수 디폴트 값이 true이기 때문에 따로 지정하지 않아도 됩니다. 만약 false이면 새로운 세션을 생성하지 않고 null을 반환합니다.

 

session.setAttribute는 세션에 로그인 회원 정보를 보관하는 방법입니다. request.addAttribute와 동작 방식이 비슷하다고 생각하면 편합니다.

 

 

@SessionAttribute

스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute를 지원합니다.

예를 들어, 이미 로그인 된 사용자를 찾는 경우에는 다음 코드를 이용해 loginMember를 조회합니다.

참고로 이 어노테이션은 세션을 생성하지 않습니다.

@SessionAttribute(name = "loginMember", required = false) Member loginMember

if(loginMember == null){
	return "home";
}

model.addAttribute("member", loginMember);
return "loginHome";

로그인 된 객체가 없는 경우 다시 홈 화면으로 이동하며, 로그인 된 객체가 있는 경우에는 모델에 담아 loginHome이라는 템플릿으로 이동합니다.

앞에서는 1. 세션을 조회하고 2. 조회한 세션을 가지고 객체를 반환받고 3. 반환 받은 객체를 모델에 넣어주는 작업을 했어햐 하는데 1번 2번 과정을 어노테이션 하나로 처리할 수 있습니다.

 

댓글