로그인처리 - 쿠키,세션

김성지·2022년 5월 29일
0

스프링기초

목록 보기
10/13

먼저 기본적인 쿠키방식으로

로그인의 상태를 어떻게 유지할 수 있을까?

쿼리 파라미터를 계속 유지하는 것은 어렵고 번거롭다.

쿠키를 사용해보자

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

모든 요청에 쿠키 정보 자동으로 포함된다.!

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

  • 영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지
  • 세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시 까지만 유지
@GetMapping("/login")
    public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
        return "login/loginForm";
    }

//    @PostMapping("/login")
    public String login(@Valid @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";
        }

        //로그인 성공 처리

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

    }

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

로그인이 성공하면 로그인 한 사용자 전용 홈 화면 만들어 주기

public class HomeController {

 private final MemberRepository memberRepository;
// @GetMapping("/")
 public String home() {
 return "home";
 }
 @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를 사용하기

로그아웃 기능 만들기

  • 세션 쿠키이므로 웹 브라우저 종료시 서버에서 해당 쿠키의 종료 날짜를 0으로 지정해주기
@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);
}

쿠키와 보안 문제

문제 1: 쿠키 값은 임의로 변경할 수 있다.
문제 2: 쿠키에 보관된 정보는 훔쳐갈 수 있다.
문제 3: 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.

대안

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

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

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

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

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

정리

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

세션을 직접 만들어 보자

  1. 세션 생성
  • sessionId 생성
  • 세션 저장소에 sessionId와 보관할 값 저장
  • sessionId로 응답 쿠키를 생성해서 클라이언트에 전달
  1. 세션 조회
  • 클라이언트가 요청한 sessionId 쿠키의 값으로, 세션 저장소에 보관한 값 조회
  1. 세션 만료
  • 클라이언트가 요청한 sessionId 쿠키으 ㅣ값으로, 세션 저장소에 보관한 sessionId와 값 제거

세션 매니저 생성하기

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

}

주요 로직은
1.createSession -> 세션 만든다.
java.util.UUID를 활용해서 sessionId 생성해줌

sessionStore에 id와 member객체 넣고

Cookie 생성 후 response에 담아주면 끝

2.getSession -> 세션 얻기

findCookie로 request에 쿠키가 담겼는지 일단 확인
sessionCookie 가 null 이면 그냥 널 반환
이후 sessionStore 즉 서버쪽 세션 저장소에서 쿠키 찾고
잇으면 맴버 객체 반환 없으면 null 반환

3.expire -> 세션 만료 시키기

V2 임

@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult
bindingResult, HttpServletResponse response) {
 if (bindingResult.hasErrors()) {
 return "login/loginForm";
 }
 Member loginMember = loginService.login(form.getLoginId(),
form.getPassword());
 log.info("login? {}", loginMember);
 if (loginMember == null) {
 bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
 return "login/loginForm";
 }
 //로그인 성공 처리
 //세션 관리자를 통해 세션을 생성하고, 회원 데이터 보관
 sessionManager.createSession(loginMember, response);
 return "redirect:/";
}

v2 로그아웃

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

v2 홈화면 처리

@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";
}

서블릿이 지원하는 세션

v3 로 바로 살펴보자

    @PostMapping("/login")
    public String loginV3(@Valid @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(true)

  • 세션이 있으면 기존 세션을 반환해줌
  • 세션이 없으면 새로운 세션을 생성해서 반환한다.

로그아웃 v3

@PostMapping("/logout")
    public String logoutV3(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        return "redirect:/";
    }

request.getSession(false)가 쓰엿다
session.invalidate() 서버쪽 세션 제거함

@SessionAttribute

    @GetMapping("/")
    public String homeLoginV3Spring(
            @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {

        //세션에 회원 데이터가 없으면 home
        if (loginMember == null) {
            return "home";
        }

        //세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }

server.servlet.session.tracking-modes=cookie

이거 넣어주기

세션 정보 확인

세션 타임아웃 설정

세션은 사용자가 로그아웃을 직접 호출해서 session.invalidate() 가 호출 되는 경우에 삭제된다.
그런데 대부분의 사용자는 로그아웃을 선택하지 않고, 그냥 웹 브라우저를 종료한다. 문제는 HTTP가 비
연결성(ConnectionLess)이므로 서버 입장에서는 해당 사용자가 웹 브라우저를 종료한 것인지 아닌지를
인식할 수 없다. 따라서 서버에서 세션 데이터를 언제 삭제해야 하는지 판단하기가 어렵다.

0개의 댓글