[ 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 #6 ] 로그인 처리1 - 쿠키, 세션 (1)

김수호·2023년 9월 18일
0
post-thumbnail

이번 섹션에서는 [로그인 처리1 - 쿠키, 세션]에 대해서 알아보자.

👉 목차는 다음과 같다.

1) 로그인 요구사항
2) 프로젝트 생성
3) 홈 화면
4) 회원 가입
5) 로그인 기능
6) 로그인 처리하기 - 쿠키 사용
7) 쿠키와 보안 문제

8) 로그인 처리하기 - 세션 동작 방식
9) 로그인 처리하기 - 세션 직접 만들기
10) 로그인 처리하기 - 직접 만든 세션 적용
11) 로그인 처리하기 - 서블릿 HTTP 세션1
12) 로그인 처리하기 - 서블릿 HTTP 세션2
13) 세션 정보와 타임아웃 설정
14) 정리

여섯 번째 섹션은 내용이 많으므로, 1) ~ 7), 8) ~ 14) 로 나눠서 포스팅하고자 한다.

바로 하나씩 확인해보자.


1) 로그인 요구사항

우리가 지금까지 만든 상품 관리 웹 애플리케이션에 다음과 같은 요구사항이 추가되었다.

  • (로그인 전) 홈 화면
    • 회원 가입
    • 로그인
  • (로그인 후) 홈 화면
    • 본인 이름(누구님 환영합니다.)
    • 상품 관리
    • 로그 아웃
  • 보안 요구사항
    • 로그인 사용자만 상품에 접근하고, 관리할 수 있음
    • 로그인 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동

 

👉 화면에 대한 정보는 다음과 같다.

  • 홈 화면 - 로그인 전
    • (로그인 전) 홈 화면에는 회원가입, 로그인 버튼이 노출된다.
  • 홈 화면 - 로그인 후
    • (로그인 후) 홈 화면에는 로그인 한 사용자의 이름이 나타나면서, 상품관리와 로그아웃 버튼이 노출된다.
  • 회원가입
  • 로그인
  • 상품 관리
    • 로그인 사용자만 상품에 접근하고, 관리할 수 있다
    • 로그인 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동해야 한다.

2) 프로젝트 생성

이전 프로젝트에 이어서 로그인 기능을 학습해보자.
이전 프로젝트를 일부 수정해서 login-start 라는 프로젝트에 넣어두었다.
( 불필요한 파일 삭제, 패키지 구조 (수정 / 설계) , HomeController 추가 정도만하였다. )

프로젝트 설정 순서

  • login-start 의 폴더 이름을 login 로 변경하자.
  • 프로젝트 임포트
    • File -> Open -> 해당 프로젝트의 build.gradle 을 선택하자. 그 다음에 선택창이 뜨는데, Open as Project 를 선택하자.
    • ItemServiceApplication.main() 을 실행해서 프로젝트가 정상 수행되는지 확인하자.
      • 정상적으로 동작함을 확인할 수 있다.

 

패키지 구조 설계

  • package 구조
    • 패키지를 설계하는 여러가지 방법이 있지만, 강의에서는 위와 같이 패키지를 설계하였다. (domain과 web을 분리하였다.)
    • 아래 참고

 

도메인이 가장 중요하다.

  • 도메인: 화면, UI, 기술 인프라 등등의 영역은 제외한 시스템이 구현해야 하는 핵심 비즈니스 업무 영역을 말함.

  • 향후 web을 다른 기술로 바꾸어도 도메인은 그대로 유지할 수 있어야 한다.

    • 이렇게 하려면 web은 domain을 알고있지만 domain은 web을 모르도록 설계해야 한다. 이것을 web은 domain을 의존하지만, domain은 web을 의존하지 않는다고 표현한다. 예를 들어 web 패키지를 모두 삭제해도 domain에는 전혀 영향이 없도록 의존관계를 설계하는 것이 중요하다. 반대로 이야기하면 domain은 web을 참조하면 안된다.

3) 홈 화면

홈 화면을 개발해보자.

👉 코드로 바로 적용해보자.

  • HomeController 수정: src > main > java > hello > login > web 패키지 내부에 HomeController 클래스를 아래와 같이 수정하자.
    • (참고) 기존에는 상품목록 화면으로 리다이렉트 하도록 되어있었다. ("redirect:/items")
  • home.html 생성 및 실행: resources/templates 디렉토리 내부에 home.html 파일을 생성한 후 실행해보자.
    • 정상적으로 홈 화면이 노출됨을 확인할 수 있다.

 

다음 내용에서는 회원 가입을 구현해보자.


4) 회원 가입

이번 내용에서는 회원 가입 기능을 구현해보자.

👉 코드로 바로 적용해보자.

  • Member 클래스 생성: src > main > java > hello > login > domain > member 패키지 내부에 Member 클래스를 생성하자.
  • MemberRepository 저장소 생성: src > main > java > hello > login > domain > member 패키지 내부에 MemberRepository 클래스를 생성하자.
  • MemberController 생성: src > main > java > hello > login > web > member 패키지 내부에 MemberController 클래스를 생성하자.
    • (참고) @ModelAttribute("member")@ModelAttribute 로 변경해도 결과는 같다. 여기서는 IDE에서 인식 문제가 있어서 적용했다.
    • 회원 가입시 검증 오류가 있으면 다시 회원 가입 페이지로 이동한다.
    • 회원 가입이 정상적으로 완료되면, 홈 화면으로 리다이렉트한다.
  • addMemberForm.html 생성: resources/templates 디렉토리 내부에 members 디렉토리를 생성하고, 내부에 addMemberForm.html 파일을 생성하자.
  • 실행해보자.
    • 홈 화면에서 회원 가입 클릭시 정상적으로 회원 가입 페이지로 이동한다.
  • 가입 양식을 작성 후 가입해보자. (검증 테스트를 위해 값을 입력하지 않고 가입해보자.)
    • 검증 처리가 정상적으로 수행됨을 확인할 수 있다.
  • 정상적으로 가입 양식을 입력 후, 가입해보자.
    • 정상적으로 가입되어 홈으로 리다이렉트 되었고, 로그도 잘 출력됨을 확인할 수 있다.

회원 정보를 메모리 기반으로 저장하고 있기 때문에, 서버를 재구동하면 데이터가 사라진다.
앞으로의 편의상 테스트용 회원 데이터를 추가하자.

 

회원용 테스트 데이터 추가

  • TestDataInit 수정 : src > main > java > hello > login 패키지 내부에 TestDataInit 클래스를 수정하자.
    • (참고) 테스트용 회원 정보: loginId : test , password : test! , name : 테스터
  • 실행해보자.
    • 서버를 띄울 때 테스트 계정이 생성됨을 확인할 수 있다.

 

다음 내용에서는 로그인 기능을 구현해보자.


5) 로그인 기능

로그인 기능을 개발해보자. 지금은 로그인 ID, 비밀번호를 입력하는 부분에 집중하자.

  • LoginService 생성: src > main > java > hello > login > domain > login 패키지 내 LoginService 클래스를 생성하자.
    • 로그인의 핵심 비즈니스 로직은, 회원을 조회한 다음에 파라미터로 넘어온 password와 비교해서 같으면 회원을 반환하고, 만약 password가 다르면 null 을 반환한다.
  • LoginForm 생성: src > main > java > hello > login > web > login 패키지 내 LoginForm 클래스를 생성하자.
  • LoginController 생성: src > main > java > hello > login > web > login 패키지 내 LoginController 클래스를 생성하자.
    • 로그인 컨트롤러는 로그인 서비스를 호출해서 로그인에 성공하면 홈 화면으로 이동하고, 로그인에 실패하면 bindingResult.reject() 를 사용해서 글로벌 오류( ObjectError )를 생성한다. 그리고 정보를 다시 입력하도록 로그인 폼을 뷰 템플릿으로 사용한다.
  • loginForm.html 생성: resources/templates 디렉토리 내부에 login 디렉토리를 생성하고, login 디렉토리 내부에 loginForm.html을 생성하자.
    • 로그인 폼 뷰 템플릿에는 특별한 코드는 없다. ( loginId , password 가 틀리면 글로벌 오류가 나타난다. )
  • 실행해보자. (정상적으로 가입된 아이디/비밀번호 입력시)
    • 입력한 로그인 ID와 비밀번호로 가입된 회원(테스트용 회원)이 있으므로, 로그인에 성공하여 홈 화면으로 이동한다.
  • 실행해보자. (틀린 정보를 입력한 경우)
    • 로그인 실패시 "아이디 또는 비밀번호가 맞지 않습니다."라는 경고와 함께 로그인 폼이 나타난다.

 

실행해보면 로그인이 성공하면 홈으로 이동하고, 로그인에 실패하면 "아이디 또는 비밀번호가 맞지 않습니다."라는 경고와 함께 로그인 폼이 나타난다.

그런데 아직 로그인이 되면 홈 화면에 고객 이름이 보여야 한다는 요구사항을 만족하지 못한다.
로그인의 상태를 유지하면서, 로그인에 성공한 사용자는 홈 화면에 접근시 고객의 이름을 보여주려면 어떻게 해야할까?

다음 내용을 통해 학습해보자.


6) 로그인 처리하기 - 쿠키 사용

이번 내용에서는 쿠키를 사용해서 로그인, 로그아웃 기능을 구현해보자.
(참고로, 쿠키에 대한 기본 개념은 모든 개발자를 위한 HTTP 웹 기본 지식 강의를 참고하자.)

 

로그인 상태 유지하기

로그인의 상태를 어떻게 유지할 수 있을까?
HTTP 강의에서 일부 설명했지만, 쿼리 파라미터를 계속 유지하면서 보내는 것은 매우 어렵고 번거로운 작업이다. 쿠키를 사용해보자.

쿠키

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

쿠키 생성

  • 참고)
    • 웹 브라우저에서 아이디와 비밀번호를 입력해서 로그인을 요청한다. (클라이언트에서 서버로 요청)
    • 서버에서는 클라이언트에서 요청한 회원 정보가 회원 저장소에 있는지 확인하여, 유효한 사용자라면, 쿠키를 만들어서 웹 브라우저에 전달한다.
    • 그러면 웹 브라우저는 내부에 쿠키 저장소라는 곳에 쿠키 정보를 저장한다.

클라이언트 쿠키 전달1

  • 참고)
    • 그 다음부터는 해당 도메인 안에서 클라이언트에서 서버로 어떤 페이지를 요청하더라도, 항상 쿠키 정보가 포함돼서 전송된다.

클라이언트 쿠키 전달2

  • 참고)
    • 모든 요청에 쿠키 정보가 자동으로 포함된다. 따라서 항상 보내주기 때문에 우리는 이것을 활용할 수 있다. (물론 보안과 관련된 이슈가 있다. 이 부분은 뒤에서 설명)

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

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

브라우저 종료시 로그아웃이 되길 기대하므로, 우리에게 필요한 것은 세션 쿠키이다.
(여기서 말하는 세션은 HTTP 세션, 서버 세션 등과 전혀 상관이 없고, 쿠키의 종류 중 하나이다.)

 

👉 이제 코드로 적용해보자.

로그인 기능

  • LoginController - login(): 로그인 성공시 세션 쿠키를 생성하자.
    • 로그인에 성공하면 쿠키를 생성하고 HttpServletResponse 에 담는다.
    • 쿠키 이름은 memberId 이고, 값은 회원의 id 를 담아둔다. 웹 브라우저는 종료 전까지 회원의 id 를 서버에 계속 보내줄 것이다.
  • 실행해보자.
  • 정상적으로 로그인이 성공했고, 로그인 클릭시 개발자 도구를 확인해보면 아래와 같다.
    • 크롬 브라우저를 통해 HTTP 응답 헤더에 쿠키가 추가된 것을 확인할 수 있다.
    • 그러면 브라우저는 그 다음부터 요청을 보낼 때, 항상 요청 정보에 쿠키 정보를 포함한다.
  • 홈 화면으로 이동해보자.
    • 크롬 브라우저를 통해 HTTP 요청 헤더에 쿠키가 추가된 것을 확인할 수 있다.
    • (참고) 다음 경로에서도 확인 가능하다.

 

👉 이제 요구사항에 맞추어 로그인에 성공하면 로그인한 사용자 전용 홈 화면을 보여주자.

  • HomeController 수정: 아래와 같이 수정하자.
    • 기존 home() 에 있는 @GetMapping("/") 은 주석 처리하자.
    • @CookieValue 를 사용하면 편리하게 쿠키를 조회할 수 있다.
    • 로그인 하지 않은 사용자도 홈에 접근할 수 있기 때문에 required = false 를 사용한다.
    • 로직 분석
      • 로그인 쿠키( memberId )가 없는 사용자는 기존 home 으로 보낸다. 추가로 로그인 쿠키가 있어도 회원이 없으면 home 으로 보낸다.
      • 로그인 쿠키( memberId )가 있는 사용자는 로그인 사용자 전용 홈 화면인 loginHome 으로 보낸다. 추가로 홈 화면에 화원 관련 정보도 출력해야 해서 member 데이터도 모델에 담아서 전달한다.
  • loginHome.html 생성: resources/templates 디렉토리 내부에 loginHome.html 파일을 생성하자.
    • th:text="|로그인: ${member.name}|" : 로그인에 성공한 사용자 이름을 출력한다.
    • 상품 관리, 로그아웃 버튼을 노출한다.
  • 실행해보자.
    • 로그인에 성공하면 사용자 이름이 출력되면서 상품 관리, 로그아웃 버튼을 확인할 수 있다. 로그인에 성공시 세션 쿠키가 지속해서 유지되고, 웹 브라우저에서 서버에 요청시 memberId 쿠키를 계속 보내준다.
    • (참고) 웹 브라우저를 종료 후 다시 접속해보면 쿠키 정보가 사라지는 것을 확인할 수 있다.

 

로그아웃 기능

이번에는 로그아웃 기능을 만들어보자. 로그아웃 방법은 다음과 같다.

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

👉 코드로 확인해보자.

  • 실행해보자.
    • 정상적으로 로그인하였고, 쿠키 정보가 생성됨을 확인할 수 있다.
  • 로그아웃 해보자.
    • 로그아웃시 홈 화면으로 리다이렉트 되고, Response Headers를 보면 Set-Cookie에 Max-Age=0 으로 들어간 것을 확인할 수 있다. (Application 탭에서 보면 쿠키 정보가 없는 것을 확인할 수 있다.)

 

쿠키만으로 (로그인 / 로그아웃) 처리를 하는 위와 같은 방식은 보안상 큰 문제가 있다.
어떤 문제를 유발하는지, 그리고 이를 해결하기 위한 방법들을 다음 내용부터 알아보자.


7) 쿠키와 보안 문제

이전 내용에서, 쿠키를 사용해서 로그인Id를 전달해서 로그인을 유지할 수 있었다.
그런데 여기에는 심각한 보안 문제가 있다.

보안 문제

  • 쿠키 값은 임의로 변경할 수 있다.
    • 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.
    • 실제 웹브라우저 개발자모드 -> Application -> Cookie 변경으로 확인
    • Cookie: memberId=1 -> Cookie: memberId=2 (다른 사용자의 이름이 보임)
    • ex) test(memberId=1, name=테스트), test1(memberId=2, name=test1) 두 계정이 있다.
      • test 계정으로 로그인해보자.
      • 쿠키 값을 변경해보자. (1 -> 2)
      • 새로고침해보자.
        • id가 2인 회원으로 바뀌어버린다.
        • 쿠키의 값은 결국 클라이언트에서 서버로 전달하는 것이기 때문에, 어떤식으로든 위변조가 가능하다. 그러므로 이런식으로 개발하면 사용자가 다 털린다..!
  • 쿠키에 보관된 정보는 훔쳐갈 수 있다.
    • 만약 쿠키에 개인정보나, 신용카드 정보가 있다면?
    • 이 정보가 웹 브라우저에도 보관되고, 네트워크 요청마다 계속 클라이언트에서 서버로 전달된다.
    • 쿠키의 정보가 나의 로컬 PC에서 털릴 수도 있고, 네트워크 전송 구간에서 털릴 수도 있다.
  • 해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.
    • 해커가 쿠키를 훔쳐가서 그 쿠키로 악의적인 요청을 계속 시도할 수 있다.

 

대안

  • 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤 값)을 노출하고, 서버에서 토큰과 사용자 id를 매핑해서 인식한다. 그리고 서버에서 토큰을 관리한다.

  • 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야한다.

  • 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게(예: 30분) 유지한다. 또는 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 된다.

 

이러한 대안책들을 적용할 수 있는 방법이 있다. 서버 세션이라는 개념을 도입하는 것이다.
다음 내용을 통해서 알아보자.


강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글