3 분 소요


로그인 - 쿠키

쿠키에 대한 개념

LoginController

Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

// ...

//쿠키에 시간 정보를 주지 않으면 세션 쿠키(브라우저 종료시 모두 종료)
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);

로그인에 성공하면 쿠키를 생성하고 HttpServletResponse에 담는다.
쿠키 이름은 memberId이고, 값은 회원의 id를 담는다. 웹 브라우저는 종료 전까지 회원의 id를 서버에 계속 보내줄 것이다.

HomeController

@GetMapping("/")
public String homeLogin(
    @CookieValue(name = "memberId", required = false) Long memberId, Model model) {

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

    // 로그인
    Member loginMember = memberRepository.findById(memberId);
    if (loginMember == null) {
        return "home";
    }

    model.addAttribute("member", loginMember);
    return "loginHome";
}
  • @CookieValue를 사용하면 편리하게 쿠키를 조회할 수 있다.
  • 로그인 하지 않은 사용자도 홈에 접근할 수 있기 때문에 required = false를 사용한다.
    • required=true는 쿠키를 조회한 결과가 null일 경우 새로운 쿠키를 생성해준다.


로그아웃

로그아웃 방법

  • 세션 쿠키이므로 웹 브라우저 종료시
  • 서버에서 해당 쿠키의 종료 날짜를 0으로 지정

LoginController

logout 기능 추가

@PostMapping("/logout")
public String logout(HttpServletResponse response) {
    expireCookie(response, "memberId");
    return "redirect:/";
}
private void expireCookie(HttpServletResponse response, String cookieName) {
    Cookie cookie = new Cookie(cookieName, null);
    cookie.setMaxAge(0);
    response.addCookie(cookie);
}

로그아웃도 응답 쿠키를 생성하는데, Max-Age=0이므로 해당 쿠키는 즉시 종료된다.


쿠키와 보안 문제

보안 문제

  • 쿠키 값은 임의로 변경할 수 있다.
    • 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.
  • 쿠키에 보관된 정보는 훔쳐갈 수 있다.
  • 해커가 쿠키를 훔쳐가서 악의적인 요청을 계속 시도할 수 있다.

대안

  • 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 토큰(랜덤)을 노출하고, 서버에서 토큰과 사용자 id를 매칭해서 인식한다.
  • 토큰이 털려도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다.


로그인 - 세션

session

  • 사용자가 loginId, password 정보를 전달하면 서버에서 해당 사용자가 맞는지 확인.
  • 사용자가 맞다면 세션 ID를 생성하는데, 추정이 불가능해야 한다.
  • UUID는 추정이 불가능
  • 생성된 세션 ID와 세션에 보관할 값(memberA)을 서버의 세션 저장소에 보관한다.

클라이언트와 서버는 쿠키로 연결이 되어야 한다.

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

[중요]

  • 중요한 포인트는 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않는다는 것.
  • 추정 불가능한 세션ID만 쿠키를 통해 클라이언트에 전달한다.

session2

  • 클라이언트는 요청시 mySessionId 쿠키를 전달한다.
  • 서버에서는 클라이언트가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션 정보를 사용한다.

정리

  • 쿠키 값 변조 가능
    • -> 예측 불가능한 세션ID 사용
  • 쿠키에 보관하는 정보는 클라이언트 해킹시 털릴 수 있다.
    • -> 세션ID가 털려도 중요한 정보가 없다.
  • 쿠키 탈취 후 사용
    • -> 서버에서 세션의 만료시간을 짧게 유지


세션 직접 만들기

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

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

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

HashMap은 동시 요청에 안전하지 않기 때문에 동시 요청에 안전한 ConcurrentHashMap을 사용했다.

LoginController

private final SessionManager sessionManager;

//...

Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

//세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
sessionManager.createSession(loginMember, response);

//...

// 로그아웃

@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
    sessionManager.expire(request);
    return "redirect:/";
}

로그인 성공시 세션을 등록하고, 세션에 loginMember를 저장해두고, 쿠키도 함께 발행한다.

HomeController

//세션 관리자에 저장된 회원 정보 조회
@GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model) {

    Member member = (Member)sessionManager.getSession(request);
    if (member == null) {
        return "home";
    }

    //로그인
    model.addAttribute("member", member);
    return "loginHome";
}

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

정리

  • 세션이라는 것은 단지 쿠키를 사용하는데, 서버에서 데이터를 유지하는 방법일 뿐이다.
  • 서블릿도 세션 개념을 지원한다.


<출처 : 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(김영한)>

댓글남기기