express-session, passport.initialize, passport.session 미들웨어 간의 상호작용 in Passport Local Authentication

HAN·2021년 5월 31일

최근에 네이티브 앱에서 세션을 기반으로한 인증을 구현할 필요가 있었는데 브라우저와 달리 메뉴얼로 세션을 핸들링해야 했다. 서버에서 받은 쿠키를 저장하고 다시 이를 서버로 보내주는 로직이었는데 하다보니 좀 더 구체적인 모습으로 세션이 어떻게 핸들링 되는지 알 필요가 있었다. 그래서 경우의 수 별로 각각 flow를 찾아 정리해 보았다. 기존 많은 설명들이 serealizeUser와 deserializeUser이 간략하게 무엇을 하는지만 설명했었는데. 대체 이들 함수 실행 전후로 무슨 일이 일어나는지 알고 싶었다. 이 글은 각 상황에서 아래 3개의 미들웨어가 어떻게 상호작용하는지에 대한 설명이다.(틀린 부분이 있다면 지적 부탁드립니다.)

express-session : 세션 복원 및 쿠키 발급/조회

sid => req.session

passport.initialize : 인증유저 초기화

req.session.passport.user => user.id

passport.session : 유저객체 복원

user.id => req.user

첫 방문

  • 서버의 express-session 미들웨어는 쿠키에 아무것도 없는 걸 확인하고 세션을 형성한다.
  • passport.initialize는 req.session이 있는지 확인하고, req.session.passport를 추가한다.
  • => 응답헤더에 set-cookie : { sid : 해시된 세션id } 를 삽입하고 해시세션id => 세션 id => 세션id : 세션 객체{~~}를 세션 저장소에 저장한다.
  • 유저는 기존에 서버로 부터 응답받은 sid를 로컬 스토리지에 저장했다가, 다음번에 같은 도메인으로 요청할 경우 요청 헤더에 해당 쿠키를 삽입해준다.

첫 로그인

  • 유저가 id, pwd를 입력해서 /login 경로로 post 요청을 할 때, 요청 헤더에는 기존에 받았던 sid가 삽입되어 전달된다.

  • express-session 미들웨어는 요청 헤더의 쿠키를 매번 확인하는데 이번에는 sid를 확인할 수 있다.

  • 이를 세션저장소(mongostore or memoryStore)에서 조회한 뒤 해당 sid(해시값)와 일치하는 세션id(실제값)를 찾아 해당 세션객체를 req.session에 세팅한다.

  • 이후 passport.authenticate() 의 로직을 통과한 인증된 user 객체가 req.login() => serealizeUser에 의해 req.session에다가 passport : { user : user.id } } 를 추가한다.(아래 사진 참고)

  • 마지막으로, 전체 유저 오브젝트를 req.user에 붙인다.

  • 이후 서버 상에서 로그인한 유저의 user 객체를 프론트로 넘겨준다.

  • 자 이때, 우리가 위에서 붙였던 req.session.passport.user = user.id 는 세션 저장소에 저장된다. req.session.save() => 자동 처리된다.

  • 서버의 응답에는 sid가 여전히 전달된다.

*세션 저장소에 저장된 세션 객체의 모습은 아래와 같다.(json)

로그인 이후의 요청

  • 쿠키가 만료되기 전까지 모든 요청에는 sid가 지속적으로 전달된다.

  • express-session 미들웨어는 요청 헤더의 sid 쿠키를 확인하고 세션 저장소에서 꺼내 세션 객체를 복원시킨다.

  • 이때, req.session의 모습은 이전에 세션저장소에 저장했던 객체의 모습 그대로다.

  • 만약 쿠키가 만료됐다면, 해당 sid는 기존의 세션 객체를 복원하지 못할 것이다.

  • passport.initialize() 미들웨어는 req.session에서 req.session.passport.user 가 있는지 확인한다.

  • 그리고 아래 passport.session()미들웨어에 user.id를 넘긴다.

  • 저장된 세션 객체 내에 passport.user는, 해당 sid에 대응하는 인증된 유저를 의미한다.

  • 즉, 인증된 유저가 없다면 세션이 만료 되었거나 뭔가 문제가 있는 것. 다시 인증을 거쳐야 한다.

  • passport.session()의 실행과정에서 아래의 deserializeUser가 실행된다.

  • 이 내부에서 해당 user.id를 deserializeUser 의 2번째 인자로 넘겨준다.

  • deserializeUser는 db에서 user.id를 조회하여 user 객체 전체를 복원한다.

  • 이후 done(null, user)를 하면, req.user가 생긴다.

쿠키 만료 혹은 로그아웃 후 요청

  • express-session은 요청 헤더의 쿠키를 확인하고 만료된 쿠키임을 감지한다. (로그아웃 이후라면 쿠키가 없는 것을 확인한다.)
  • express-session은 새로운 세션(쿠키)를 생성하여 세션저장소에 저장하고 sid를 응답 헤더에 담는다.
  • passport.initialize는 세션이 만료되었으므로 기존 요청으로부터 req.session.passport.user를 확인할 수 없다.
  • 로그인 페이지로 돌려서 재 인증을 유도하든지, 아니면 새로운 로그인이 필요한 페이지라고 경고를 띄운다.

참고(사실상 아래 두 글을 상호 참조하여 연결고리를 이을 수 있었다..)

미들웨어의 순서가 포인트. 인증 요청이 언제 시작되고 어떻게 진행되는지 아는게 중요합니다.

  • 세션 미들웨어는 세션을 생성한다.(세션 스토어의 데이터를 사용)
  • passport.initialize는 request 객체에 _passport 객체를 할당하고, request 객체에 session 객체가 있는지, 그리고 있다면 그 하위에 passport 필드가 있는지(없다면 생성한다.) 확인한 뒤, _passport에 저장했던 request 객체 하위에도 session 객체를 할당한다. 그렇게 되면 아래와 같은 모습일 것이다.

    따라서, 세션 필드(req._passport.session)는 req.session.passport에 할당된 객체를 참조하게 된다.
  • passport.session은 req._passport.session 안에 있는 user필드를 찾는다. 그리고 만약 찾았다면, deserializeUser 함수를 실행하면서 deserializeUser 함수에 user를 인자로 전달한다. deserializeUser 함수는 req._passport.session.user를 request 객체의 user 필드에 할당한다.(req._passport.session.user가 있는 경우에만)

    만약 위와같이 serializeUser에서 user를 stringify를 했다면, deserializeUser 에서는 parse해주어야 한다.

  • deserializeUser 함수는 패스포트를 설치할 때 너의 콜백을 _deserializeUsers 함수 스택에 놓기 위해 처음 호출된다. 두번째로 실행되는 건, passport.session 미들웨어에서 request 객체에 user 필드를 할당하기 위해 호출된다. 우리가 deserializeUser 안에 정의한 콜백은 request 객체에 user필드가 할당되기 전에 실행된다.

  • serializeUser 역시 패스포트가 설치될 때 처음으로 호출된다. 하지만 이는 user 객체를 직렬화 해서 세션에 저장하기 위해서 이용될 뿐이다. 두번째로 실행되는 건, Passport에 의해 할당된 login / logIn (별칭) 메서드(역자 : req.login()을 말하는 듯)에서 호출되어 세션 내에 사용자 객체를 저장하는 데 사용됩니다. serializeUser 함수는 또한 _serializers 스택 안에 이미 내장된 함수( 우리가 패스포트를 설치할 때 할당 되어진 것)가 있는지도 확인하고 실행합니다.

  • 그러면 user 객체가 할당되거나, user.id가 req._passport.session.user에 할당됩니다. session 필드가 직접적으로 req.session.passport 필드를 참조한다는 잊으면 안됩니다. 그럼으로써, user가 세션 내에 저장됩니다.(왜냐하면 req._passport.session은 req.session.passport를 참조하고, req._passport.session은 매 요청마다 passport.initialize 미들웨어에 의해 수정되기 때문입니다. 요청이 끝나면, req.session 데이터는 세션 스토어에 저장될 것입니다.

  • 성공적으로 인증을 마친 뒤, 그 다음 요청이 시작될 때 어떤 일이 일어날까요?

  • 세션 미들웨어는 이미 유저 데이터가 저장된 세션 스토어로부터 세션을 얻는다.

  • passport.initialize는 어디에? 세션이 있는지 확인하고, req.session.passport를 req_passport.session에 할당한다.

  • passport.session은 req._passport.session.user 를 확인하고 deserialize 한다. 이 단계에서, 우리는 req.user를 얻고, req.isAuthenticated는 true를 리턴한다.(req._passport.session.user가 있다면)

profile
즐거운 배움이 되길

0개의 댓글