JWT를 활용한 로그인 상태 유지 (with Access Token, Refresh Token)

백수만·2023년 3월 28일

프로젝트

목록 보기
3/6
post-thumbnail

글을 읽는 대상: 🤔 JWT를 발급하는 로그인 상태 유지 방법으로 Access Token, Refresh Token을 사용하는 데 어디에 저장하면 좋을지 고민하는 독자

🚪 Introduction

배경
팀 프로젝트를 하면서 로그인 기능을 구현하였다. 로그인 기능을 구현하면서 어떤 고민과정을 거쳤는지 정리해서 글을 적어보겠다.

목적

  • Access Token, Refresh Token을 클라이언트에서 어디에 저장하면 좋을지에 대한 하나의 의견을 듣고 고민할 수 있다.
  • API 요청 시 어떤 검증 로직을 사용했는지 알 수 있다.
  • 결국 완벽한 보안은 없다는 것을 알 수 있다.

🔐 로그인 상태 유지 방법

로그인 상태 유지를 위해 다음과 같은 방식을 사용할 수 있다.

  • Session id를 발급하는 방법
  • JWT을 발급하는 방법

Session 방식의 장단점

  • 장점
    • 정보 자체는 서버에 저장되어 있어 중요한 정보를 클라이언트에게 노출되지 않도록 할 수 있다.
  • 단점
    • 해커가 이를 탈취하여 클라이언트인척 위장할 수 있다.
    • 요청이 많아지면 서버에 부하가 심해진다.

JWT 방식의 장단점

  • 장점
    • 데이터 위변조를 막을 수 있다.(JWT Signature 필드를 통해 위변조 여부 확인 가능)
    • 인증 정보에 대한 별도의 저장소 필요 없음
  • 단점
    • 토큰을 탈취당하면 대처가 어렵다. → 유효기간 만료될 때까지 계속 사용
    • Payload 자체는 암호화가 되지 않기 때문에 유저의 중요한 정보는 담을 수 없다.(하지만 JWT 자체는 암호화 된다.)

Session id로 발급하는 방법은 세션 정보를 서버 내부나 DB에 저장하는데 매 요청마다 접근하면 서버 부하가 높아진다는 단점이 있다. 그래서 필자는 프로젝트에서 Session을 사용하지 않고 JWT 방식을 사용하였다.
그러면 JWT를 어떻게 사용하는지 알아보자

🔐 JWT 발급 방법

JWT를 활용하여 Access Token과 Refresh Token을 발급받는 방식으로 로그인을 구현하였다.

Access Token : 리소스에 접근하기 위해 사용되는 토큰
Refresh Token : Access Token이 만료되었을 때 갱신하기 위한 토큰

발급받은 토큰을 클라이언트에서 가지고 있는 방법으로는 Cookie에 저장하는 방법과 in-memory에 저장하는 방법이 있다. 필자는 Access Token은 in-memory에 저장, Refresh Token은 Cookie에 저장하는 방식으로 구현하였다. 왜 이런 방식으로 저장했는지 아래에서 추가적으로 설명하겠다.

🔐 Access Token을 in-memory(자바스크립트 변수)에 저장한 이유

Access Token을 in-memory에 저장한 이유는 다음과 같다.

  • 해커가 CSRF 공격을 하려고 해도 쿠키에는 Access Token이 없기 때문에 인증 불가능 상태가 되어 공격 불가능 하다.
    • CSRF 공격이란 Cross Site Request Forgery로 해커가 내 인증정보를 활용해서 서버에 나인것처럼 속이는 것이다.
  • Access Token은 클로저를 활용한 로컬변수에 저장되어 외부에서 접근할 수 없어서 해커가 XSS 공격을 하여 탈취할 수 없다.
    • XSS 공격이란 해커가 공격하려는 사이트에 악성 스크립트를 삽입하여 민감한 정보를 탈취하는 것이다.

하지만 장점만 존재한 것이 아니라 단점도 존재한다. 새로고침 시 Access Token이 없어져 다시 받아와야 한다. 이 때 사용자 경험(UX)을 고려하여 클라이언트에서 Refresh Token을 활용한 Silent Token Refresh 방식으로 받아온다.

🔐 Refresh Token을 Cookie에 저장한 이유

Access Token을 in-memory에 저장함으로 새로고침 시 Token이 없어진다. 이 때 Refresh Token도 없어지면 결국 다시 로그인을 해야하는 불편함이 발생한다. 그래서 Refresh Token은 새로고침에도 지워지지 않도록 Cookie에 저장했다. Cookie로 저장하되 Cookie 옵션을 줘서 보안을 강화했다. 사용한 옵션은 다음과 같다.

  • Secure: HTTPS 프로토콜에서 암호화된 요청일 경우에만 전송
  • HttpOnly: 자바스크립트에서 쿠키에 접근할 수 없다.
  • path: /api/user/auth/refresh 에서만 접근 가능하도록 하였다.

하지만 Cookie에 저장되면 탈취될 수 있다는 문제점이 있다. 이럴 경우에 대처할 방법이…😱😱 생각만해도 끔찍하다

탈취되는 경우를 고려하여 로그인 사용자마다 한 개의 Refresh Token을 매핑(로그인 시 Refresh Token을 DB에 저장)하고 Refresh Token이 유효한지 검증하는 로직을 추가하였다.

🧐 고민 사항
Refresh Token을 DB에 저장하여 접근하면, Session을 사용하는 것과 비슷하게 서버에서 DB에 접근하니 부하가 발생하지 않을까?

아니다. Session은 요청이 올 때마다 DB에 접근하는 반면 Refresh Token은 재발급할때만 DB에 접근하기 때문에 접근 빈도가 적어서 Session 보다 부하가 덜 든다.

+) DB에 Refresh Token을 저장했는데 DB로는 NoSQL인 Redis에 저장하는 방식이 좋다고 생각한다. 이유는 빠른 접근 속도를 가진다는 장점과 데이터가 휘발되더라도 어플리케이션이 동작하는데 큰 문제가 발생하지 않다는 점이다.

⚙️ 로직 흐름도

[api 요청]

  • [클라이언트] access token이 필요한 요청을 보낸다.
  • [api 서버]
    • access token이 유효하면 정상적으로 요청을 처리한 후 응답을 보낸다.
    • access token이 유효하지 않으면 unauthorized 401 응답을 보낸다.

[유효하지 않은 Access Token]

  • [클라이언트] access token 발급 요청을 보낸다.
  • [api 서버]
    • DB에서 사용자와 매핑된 refresh token 정보와 쿠키의 refresh token을 비교하여 유효한지 확인한다.
    • refresh token이 유효하면 access token을 재발행 후 payload로 응답을 보낸다.
    • refresh token이 유효하지 않으면 unauthorized 401 응답을 보낸다.

[유효하지 않은 Refresh Token]

  • [클라이언트] 로그인 요청을 보낸다.
  • [api 서버] 비밀번호를 확인하고 응답을 보낸다.

🫠 마치며

한계점으로, Refresh Token이 탈취당했는지 알 수 있으면 폐기하는 방법까지 추가하면 더 좋았을 텐데 위 방법은 그렇지 못하여 완전한 보안이라고 할 수는 없다.

공부하다보니 완전한 보안은 없는 것 같다는 느낌이 들었다.🤯 (어느정도 trade-off가 존재하였다.)

0개의 댓글