스프링-로그인 api 구현-세션-(2)

이진우·2023년 7월 26일
1

스프링 학습

목록 보기
6/41

김영한 강사님의 스프링 MVC 강의 2편을 듣고 수정하고 정리한 내용입니다.

쿠키보단 세션

저번 포스트에서 쿠키의 단점을 적었었다. 간단히 요약하면 중요한 정보를 모두 서버에 저장하는 방식으로 하는 것이 좋은데 클라이언트와 서버는 추정 불가능한 임의의 식별자값으로 연결해야 한다. 이렇게 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라 한다.

세션 개념

위 내용을 텍스트로 풀면

1)사용자가 로그인을 시도
2)로그인이 성공하면 서버에서 세션 ID를 생성한다.
3)서버 내부에 세션 저장소에 세션 ID를 key,로그인한 사용자를 value값으로 둔다.


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

쿠키와 다른 점

  • 회원과 관련한 정보가 클라이언트에 전달되지 않는다.
  • 오직 추정 불가능한 세션ID만 쿠키를 통해 클라에 전달
  • 클라는 요청시에 항상 mySessionID 쿠키를 전달
  • 서버는 클라가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해서 로그인시 보관한 세션 정보를 사용
  • 세션 정리

    세션이 뭔가 특별한 것이 아니라 세션은 단지 쿠키를 사용하는데 , 서버에서 데이터를 유지하는 방법이라는 것이다.

    서블릿 HttpSession이용 로그인 구현

    먼저 기본 domain,repository,Service는 전에 구현해 놓은 것을 사용한다.아래는 그 코드의 링크이다.

    먼저 Filter를 수정한다.

    LogCheckFilter
    @Slf4j
    public class LoginCheckFilter implements Filter {
    
        private static final String[] whitelist={"/members/add","/members/homeBySession","/members/homeByCookie","/loginByCookie","/logoutByCookie","/loginBySession","/logoutBySession"};
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            Filter.super.init(filterConfig);
        }
    
        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest httpRequest=(HttpServletRequest) request;
            String requestURI=httpRequest.getRequestURI();
            HttpServletResponse httpResponse=(HttpServletResponse) response;
            try{
                log.info("인증 체크 필터 시작 {}",requestURI);
                if(isLoginCheckPath(requestURI)){
                    log.info("인증 체크 로직 실행 {}",requestURI);
                    HttpSession session=httpRequest.getSession(false);
                    Cookie []cookies=httpRequest.getCookies();
                     if(cookies!=null){
                         for (Cookie cookie : cookies) {
                             log.info("cookie의 이름 {}",cookie.getName());
                             log.info("cookie의 value {}",cookie.getValue());
                         }
                     }
                     else{
                         log.info("쿠키는 null입니다");
                     } if(session==null||session.getAttribute("loginMember")==null){
                        log.info("미인증 사용자 요청 {}",requestURI);
                        return;
                    }
                }
                chain.doFilter(request,response);
            }catch (Exception e){
                throw e;
            }finally {
                log.info("인증 체크 필터 종료 {}",requestURI);
            }
        }
        public boolean isLoginCheckPath(String requestURI){
            return !PatternMatchUtils.simpleMatch(whitelist,requestURI);
        }
    }

    위 코드에서 HttpSession을 이용해서 session이 null이거나 session의 loginMember라는 쿠키이름을 찾아 그 값이 null 이라면 로그인하지 않은 사용자라 판단한 후 접근할 수 없도록 한다. 또한 Cookie로 cookieName과 cookie값을 파악해보도록 하겠다.

    참고로 HttpSession session=httpRequest.getSession(false)는 session이 있으면 가지고 오고 없으면 아무것도 하지 않는다. default값은 true인데 true인 경우 session을 아예 생성해버리니 이에 유의해야 함.

    LoginController
    @RestController
    @RequiredArgsConstructor
    @Slf4j
    public class LoginController {
        private final LoginService loginService;
        @PostMapping("/loginByCookie")
        public String loginByCookie(@Valid @RequestBody MemberSignInDto memberSignInDto,HttpServletResponse response){
            System.out.println(memberSignInDto.getLoginId()+ " "+memberSignInDto.getPassword());
            Member loginMember=loginService.login(memberSignInDto.getLoginId(), memberSignInDto.getPassword());
            log.info("login? {}",loginMember);
            if(loginMember==null){
                return "로그인 실패";
            }
            Cookie idCookie=new Cookie("memberId",String.valueOf(loginMember.getId()));
            //헤더가 Cookie이고 value는 memberId=1인 상황이다.
            response.addCookie(idCookie);
            return "로그인 성공";
        }
    
    
        @PostMapping("/logoutByCookie")
        public String logoutByCookie(HttpServletResponse response){
            expireCookie(response,"memberId");
            return "로그아웃 완료";
        }
        public void expireCookie(HttpServletResponse response,String cookieName){
            Cookie cookie=new Cookie(cookieName,null);
            cookie.setMaxAge(0);
            response.addCookie(cookie);
        }
    
    
        //------------------------------------------------------------------------------------------------
        @PostMapping("/loginBySession")
        public String loginBySession(@RequestBody MemberSignInDto memberSignInDto,HttpServletRequest request){
           
            Member loginMember=loginService.login(memberSignInDto.getLoginId(), memberSignInDto.getPassword());
            log.info("login? {}",loginMember);
            if(loginMember==null){
                return "오류 발생";
            }
            HttpSession session=request.getSession();//세션이 있으면 있는 세션 반환, 없으면 신규 생성 default가 true이고 true일 떄
            //request.getSession(false):세션이 없으면 새로운 세션 생성 X, 세션이 있으면 반환
            session.setAttribute("loginMember",loginMember);//하나의 세션에 여러 값을 저장할 수 있다.,session에 loginMember라는 속성 추가
            return "로그인 성공";
        }
    
        @PostMapping("/logoutBySession")
        public String logoutBySession(HttpServletRequest request){
            HttpSession session=request.getSession();
            if(session!=null){
                session.invalidate();//세션을 제거한다.
            }
            return "로그아웃 완료";
        }
    }

    밑에 -----------가 있는데 그 아래가 session을 이용해서 login과 logout을 구현한 것이다.

  • session.setAttribute
  • session.setAttribute("loginMember",loginMember);를 사용하면 loginMember라는 속성을 session에 추가한다고 생각하자. 그리고 그 속성에 로그인을 성공한 member가 들어있다.
    MemberController 추가
    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/members")
    public class MemberController {
        private final MemberRepository memberRepository;
        @PostMapping ("/add")
        public String addForm(@RequestBody @Valid MemberSignUpDto memberSignUpDto){
            Member member=new Member(memberSignUpDto.getName(),memberSignUpDto.getLoginId(),memberSignUpDto.getPassword());
            memberRepository.save(member);
            return "저장되었습니다";
        }
    
        @GetMapping("/homeByCookie")
        public String homeLoginByCookie(@CookieValue(name = "memberId",required = false)Long memberId){
            if(memberId==null){
                return "기본 홈 화면";
            }
            Member loginMember=memberRepository.findById(memberId).get();
            if(loginMember==null){
                return "기본 홈 화면";
            }
            String memberName=loginMember.getName();
            return memberName+"을 위한 기본 홈 화면";
    
        }
        @GetMapping("/homeBySession")
        public String homeLoginBySession(HttpServletRequest request){
            HttpSession session= request.getSession(false);
            if(session==null){
                return "기본 홈 화면";
            }
            Member loginMember=(Member)session.getAttribute("loginMember");
            if(loginMember==null){
                return "기본 홈 화면";
            }
            return loginMember.getName()+"을 위한 기본 홈 화면";
        }
    
    
    
    
    }

    위의 코드의 의미는 request에서 session을 가져올 때 session이 null이거나 loginMember의 속성이 없을 떄는 기본 홈 화면이라는 글자를 반환하고 그렇지 않으면 loginMember의 이름+"을 위한 기본 홈 화면"을 반환한다.

    SessionInfoController
    @Slf4j
    @RestController
    public class SessionInfoController {
        @GetMapping("/session-info")
        public String sessionInfo(HttpServletRequest request){
            HttpSession session=request.getSession(false);
            if(session==null){
                return "세션이 없습니다";
            }
            System.out.println(session.getAttributeNames());//HttpSession 객체에서 현재 세션에 저장된 모든 속성의 이름(키)을 가져오는데 사용되는 메서드
            session.getAttributeNames().asIterator().forEachRemaining(name->log.info("session name={},value={}",name,session.getAttribute(name)));
            //name에는 loginMember가 들어간다.,session.getAttribute(name)에는
            log.info("sessionId={}",session.getId());//031AF55
            log.info("maxInactiveInterval={}",session.getMaxInactiveInterval());
            log.info("creationTime={}",session.getCreationTime());
            log.info("lastAccessedTime={}",new Date(session.getLastAccessedTime()));
            log.info("isNew={}",session.isNew());
            return "세션 출력";
        }
    }

    위 코드는 session에 대해서 알아보기 위해 작성한 코드이다.

    postman으로 세션 로그인 테스트 하기


    위에서 보듯 로그인을 하지 않은 상태라면 items에 접근할 수 없고 기본 홈 화면만 출력되는 것을 볼 수 있다.

    이제 회원가입과 로그인을 시도해보자

    위에서 보듯이 request 에 cookie가 들어가고 JSESSIONID=... 이 들어가는 것을 볼 수 있다.

    이제 items를 들어가보면

    정상적으로 문자열이 출력되고

    이름+"을 위한 기본 홈 화면" 이 출력되는 것을 볼 수 있다.

    아까 Filter에서

     Cookie []cookies=httpRequest.getCookies();
                     if(cookies!=null){
                         for (Cookie cookie : cookies) {
                             log.info("cookie의 이름 {}",cookie.getName());
                             log.info("cookie의 value {}",cookie.getValue());
                         }
                     }
                     else{
                         log.info("쿠키는 null입니다");
                     }

    이 코드가 실행되는 것을 보자.

    2023-07-26 21:40:16.788  INFO 21316 --- [nio-8080-exec-7] c.l.C.configuration.filter.LogFilter     : REQUEST [938282c5-288f-4eca-8bc8-36bd7d819ac5][/items]
    2023-07-26 21:40:16.788  INFO 21316 --- [nio-8080-exec-7] c.l.C.c.filter.LoginCheckFilter          : 인증 체크 필터 시작 /items
    2023-07-26 21:40:16.788  INFO 21316 --- [nio-8080-exec-7] c.l.C.c.filter.LoginCheckFilter          : 인증 체크 로직 실행 /items
    2023-07-26 21:40:16.788  INFO 21316 --- [nio-8080-exec-7] c.l.C.c.filter.LoginCheckFilter          : cookie의 이름 JSESSIONID
    2023-07-26 21:40:16.788  INFO 21316 --- [nio-8080-exec-7] c.l.C.c.filter.LoginCheckFilter          : cookie의 value BCFEDFAF9744BE3B6C1F2EA9780AF669
    2023-07-26 21:40:16.788  INFO 21316 --- [nio-8080-exec-7] c.l.C.c.filter.LoginCheckFilter          : 인증 체크 필터 종료 /items
    2023-07-26 21:40:16.788  INFO 21316 --- [nio-8080-exec-7] c.l.C.configuration.filter.LogFilter     : RESPONSE [938282c5-288f-4eca-8bc8-36bd7d819ac5][/items]

    로그가 이렇게 찍힌다.

    즉 session 도 cookie 기반이라는 것을 보다 직관적으로 확인할 수 있다.
    cookie의 이름이 JSESSIONID라는 것과 cookie의 getValue를 하면 그 복잡한 String 이 나온다는 것을 알 수 있다.


    또한 session-info를 출력하여 보면

    이렇게 나온다는 것을 알 수 있다.
    위에서 session에는 여러 AttributeNames를 가질 수 있고
    session의 name은 우리가 설정한 loginMember이고, 그 값은 Member 객체를 의미한다는 것 또한 알 수 있다.

    또한 session의 ID가 위에서 cookie의 value값과 같은 "그 긴거" 라는 것을 확인할 수 있다.

    일단 마저 로그아웃을 테스트해보자.


    이제 items 에 접근할 수 없다.

    정리

    일단 위의 내용을 토대로 정리를 해보자.
    1)cookie.getName()은 JSESSIONID이다.
    2)session.getId()==cookie.getValue() 이고 그 값은 BCFEDFAF9744BE3B6C1F2EA9780AF669 이다.
    3)session.getAttributeNames로 session의 속성을 각각 탐색할 수 있는데 session.getAttribute(name)을 한다면 실제 값인 Member가 튀어 나온다.

    이제 텍스트로 정리를 해보겠다.
    1)사용자가 로그인 요청을 한다

    2)로그인에 성공시 서버는 session에 loginMember라는 속성을 추가하고 로그인한 회원을 저장한다.
    3)이 과정에서 서버는 세션 저장소에 세션 ID(ex:zz0101..)를 생성(key값)하고 value로는 로그인한 회원을 저장해 둔다.
    4)이제 response에 쿠키를 넣어서 보내는데
    Cookie idCookie=new Cookie("JSESSIONID","zz0101..");을 보내는 느낌이다.
    5)앞으로 클라이언트는 무언가 보낼 떄 이 쿠키값을 가지고 간다
    6)서버는 이 쿠키의 값("zz0101")(세션에서 key) 과 session.getAttribute("loginMember")로 어떤 값을 꺼내와야(로그인멤버) 할지 알고 그 것을 꺼내온다.

    HttpSession기반의 한계,단점

  • HttpSession은 기본적으로 쿠키에 세션 ID를 저장하여 사용. 이는 쿠키가 안전하지 않은 환경에서 탈취당할 수 있으며, 이로 인해 세션 하이재킹 등의 보안 위협이 발생
  • 즉 보안적으로 개발자가 할게 많다.
    그래서 스프링 시큐리티를 이용해서 더욱 쉽고 안전하게 회원가입, 로그인 기능을 사용할 수 있다.

    profile
    기록을 통해 실력을 쌓아가자

    0개의 댓글