[BiP] 로그인 로직 변경 기록

찐새·2023년 10월 15일
0

개인 프로젝트

목록 보기
3/3
post-thumbnail

프리온보딩 FE 챌린지 10월 - 로그인 기능 구현, 하나부터 열까지! 강의를 들으면서 내가 만든 개인 프로젝트의 로그인 로직이 계속 신경 쓰였다. 만들고 싶었던 기능에만 치중해 기본이면서도 가장 중요한 로그인을 대충 만들었기 때문이다. 얼마나 대충 만들었는지 보면 깜짝 놀라리라.

아무튼 강의에서 들은 내용을 참고해서 봐주기 힘든 로그인 과정을 그나마 볼 만한 모양새로 고친 과정을 기록해 보기로 한다.

유저 식별과 로그인 유지

로그인의 목적은 유저에게 서비스 이용 권한이 있는지 식별하고, 유저의 행위를 제어하고 관리하기 위함이다. 등록되지 않은 유저가 서비스의 CRUD 요청을 멋대로 해서는 안 되고, 권한이 없는 페이지에 접근해서도 안 된다.

식별된 유저라면 서비스를 계속 이용할 수 있도록 인증 상태를 유지해야 한다. 로그인하고 A서비스 이용하고, 로그인하고 B서비스 이용하는 식이라면... 누구도 이용하지 않는 서비스가 될 것이다.

나 역시 이러한 점을 고려해 로그인을 구현했다. 표면적으로는 말이다.

변경 전 로그인 로직

초기 로직을 한 눈에 보기 위해 시퀀스 다이어그램을 그려봤다.

엉망진창이지만 생애최초 다이어그램이니 넘어가자.

먼저 로그인 유지는 다음 과정을 따랐다.

  1. 서비스에 접속하면 로컬 스토리지에 저장한 토큰을 확인한다.
    • 토큰이 없으면 로그인 페이지로 이동한다.
  2. 토큰이 있다면 쿼리 파라미터에 담아 서버의 /auth/check로 식별을 요청한다.
  3. 서버에서 토큰에 해당하는 유저를 확인한 후 유저 정보를 담은 응답을 보낸다.
    • 유저 정보가 없다면 로그인 페이지로 이동한다.
  4. 이후 서비스는 로컬 스토리지에 저장된 토큰을 이용한다.

로그인 과정은 다음과 같다.

  1. 구글 로그인 버튼을 누르면 서버의 /auth/google/callback으로 이동하는 창을 연다.
  2. 서버에서 passport의 google oauth20 전략을 이용해 소셜 로그인을 시도한다.
    • 유저의 email과 profile을 받아온다.
    • 기존 유저가 있다면 유저 정보를 서버 세션에 담고, 없다면 생성 후 담는다.
  3. 처리가 끝나면 로그인 페이지로 리다이렉트한다.
  4. 로그인 페이지로 돌아오면 서버에 /auth/login 요청을 보내 세션에 담긴 유저 정보를 응답에 담아 클라이언트로 보낸다.
  5. 받은 응답이 올바르면 유저의 id(!!)를 로컬 스토리지에 토큰으로 저장하고 home으로 푸시한다.
    • 아니라면 로그인 페이지에 남겨 둔다.

누군가 옆에서 봤다면 곧장 '으악!'했을 로직이다. 문제 되는 부분을 스스로 짚어 보자.

보안 문제

먼저 유저의 정보라고 할 수 있는 부분을 암호화도 하지 않은 채 로컬 스토리지에 저장한 것이 가장 큰 문제였다. 프로필에 담긴 속성 중 sub를 사용했다. 고유값이긴 하지만 숫자로만 되어 있어서 별로 중요하지 않다고 판단했기 때문이었다. 프로젝트 자체도 털릴 게 없었고.

하지만 이 자체가 위험한 생각이었다. 보안 측면에서 보면 그 내용이 뭐가 되었든 유저의 정보에 해당한다면 숨기는 게 맞았다. 애초에 uuid 등으로 id를 해시값으로 만들었다면 문제가 덜했겠지만, 아무튼 프론트엔드 개발만 생각해 서버쪽은 대충 구현했다.

세션 사용

로그인 유지를 위해서 초기에 세션 방식으로 구현했다. 잘못된 선택은 아니었지만, 그렇다고 특별한 이유가 있지도 않았다. 나아가 REST한 방식으로 API를 구현했다고 생각했는데, 따지고 보면 세션은 REST하지 않은 느낌이 들었다.

REST API의 원칙 중 하나는 클라이언트와 서버 간의 stateless이다. 클라이언트가 서버의 상태를 가지면 안 되고, 서버도 클라이언트의 상태를 가지고 있으면 안 된다. 그러나 세션 방식은 만료 기한이 있다손 치더라도 서버가 클라이언트의 정보를 가지고 있는 셈이다. 내 생각과 구현을 일치시키기 위해서 세션 방식을 쳐냈다.

불필요한 로그인 요청

변경할 방식을 이렇게 저렇게 생각하다 보니 login 요청도 말이 안 됐다. 페이지 랜딩 시 유저 정보를 검사하는 함수를 실행한다. 로그인 시도 후 페이지로 돌아오면 로그인 함수도 실행되고, 검사 함수도 실행된다. 어쩌다 이런 멍청한 코드를 짠 건지 의문이 드는 대목이었다. 지금도 멍청하지만, 예전에는 더했다. 이것도 고치기로 했다.

이 문제들에 중점을 두고 로직을 변경하기 시작했다.

변경 후 로그인 로직

변경한 로직의 시퀀스 다이어그램이다. 수정한 부분을 중점으로 보자.

JWT를 활용한 토큰 방식 사용

세션 방식을 버리고 refresh tokenaccess token을 사용하는 토큰 방식을 채용했다. 구글 로그인을 요청하면 성공 콜백함수에서 refresh token을 생성하여 응답 쿠키에 담아 보낸다. 이후 로그인 페이지로 redirection되고, 서버에 유저 정보를 체크하는 요청을 보내는 check 함수가 실행된다.

서버에서는 헤더의 Authorization에 담아 보낸 Bearer access token을 확인한다. access token이 유효하면 유저 정보와 기존 토큰을 응답한다. 만약 access token이 없거나 유효하지 않다면 쿠키에 담긴 refresh token을 검사한다. 유효하다면 새로운 access token을 발급하지만, 이것마저 유효하지 않다면 에러을 응답한다.

발급 받은 access token은 이전과 마찬가지로 로컬 스토리지에 저장했다. 차이점은 유저 식별 정보가 암호화되었다는 것과 토큰에 만료 기한이 있어 영구 보관이 되지 않는다는 점이다. 이러한 check 과정이 모든 페이지에서 실행되도록 했다. 덕분에 위에서 언급한 불필요한 로그인 요청을 제거할 수 있었다.

느낀점

코드는 크게 의미 있는 것 같지 않고, 너무 못생겨서 첨부하지 않았다. 코드 보다 로직을 이해하는 게 더 중요하기도 하고.

간단하게만 생각했던 로그인이 전혀 간단하지 않음을 깨달았다. 그나마 내 프로젝트 규모가 작아서 사용자 식별만 생각하면 되는 것이지, 다른 큰 서비스처럼 admin이 나뉘고, 다른 도메인과 연결하고 하다 보면 진짜 어마어마하게 복잡할 것이다.

또 서버 코드를 직접 변경하면서 어떤 식으로 요청을 주고 받는지 더 깊게 이해할 수 있었다. 이전에는 단순하게 클라이언트에서 요청을 보내면 서버에서 그 요청에 해당하는 응답만 해주면 된다고 생각했다. 그래서 변경 전 로직이 그 모양이었고. 이제는 내가 어떻게 요청을 보내야 서버에서 어떤 응답을 하는지 사전에 고민해 보고, 적절한 에러핸들링까지 포함한 코드를 작성할 수 있을 것 같다.

로그인 뿐만 아니라 본 서비스에 대한 요청과 처리도 이번에 배운점을 이용해서 더 나은 코드로 리팩토링해 봐야겠다. 로직과 코드가 일치하는 개발자를 목표로 하자.

profile
프론트엔드 개발자가 되고 싶다

0개의 댓글