refresh token 도입기
- ❗ SSR 상에서 refresh token을 도입하면서 느낀 것들을 작성한 글입니다.
- ❗ SSR에서 로그인이 어떻게 이루어지는지 궁금하시면 여기를 참고해주세요!
도입 계기 - 2시간이 지나면 로그인이 풀린다!
현재 진행하고 있는 놀토 앱은 access token 유효기간인 2시간을 지나면 무조건 로그아웃되고 있었다. 이 현상은 반드시 고쳐져야 할 이슈였다. 특히 글쓰기 기능에서 치명적이었는데, 글을 쓰면서 화면을 2시간 이상 유지하고 있다가 글 작성하기 버튼을 누르면 기존 글 내용이 전부 날아가기 때문이었다.
access token 만료기간을 2주로 늘리면 안 되나요?
access token은 JWT이므로 그 자체로 인증 정보를 모두 가지고 있어서 탈취되면 매우 위험한 상황이 발생할 수 있다.
access token은 서버에 저장되지 않으므로(stateless) token이 탈취되면 서버에서는 막을 방법이 없다.
만료 기간이 2주인 access token을 탈취한 해커는 2주 동안 정상 사용자 행세를 할 수 있어서 access token의 만료 기간은 짧은 것이 좋다.
XSS와 CSRF를 잘 막아두면 access token은 절대로 탈취될 일이 없지 않나요?
XSS와 CSRF는 웹에서 발생할 수 있는 가장 대표적인 취약점이다. XSS는 자바스크립트 코드를 사용자 브라우저에서 실행하여 악의적인 작업을 수행할 수 있는 공격이고, CSRF는 사용자 브라우저에서 어떤 api로의 요청을 강제함으로써 사용자가 의도하지 않은 작업을 수행하는 공격을 의미한다. 이런 문제를 잘 막아두면 access token이 탈취될 것이라는 가정을 할 필요가 없는 것이 아닐까?
하지만
항상 보안 취약점이 없다고 생각하는 것은 굉장히 위험한 생각이다. react를 사용하면 XSS 공격을 방지해주고, localStorage를 사용하면 CSRF 취약점을 막아주지만, 이것이 항상 통할 것으로 생각하면 안된다. 예를 들어 SSR 방식의 렌더링을 사용할 때, html의 script 태그로 서버의 데이터를 클라이언트로 내려줄 수가 있는데 이런 경우도 XSS 문제가 발생할 수 있다.
그래서 항상 우리가 예측할 수 없는 보안 취약점이 발생할 수 있으므로, 탈취되었을 때를 가정하고 최대한 덜 치명적인 방향으로 개발하는 습관을 가져야 한다.
해결책 - refresh token을 도입해보자!
refresh token은 access token을 재발급받을 수 있는 token이다. 이 token은 서버에 저장되기 때문에(stateful) refresh token이 해커에 의해 탈취당했다고 판단되었을 때 서버에서 refresh token을 삭제함으로써 강제 로그아웃을 시킬 수 있다.
이런 특징을 이용해서 access token + refresh token의 조합을 구성하면 access token의 경제적인 장점
과 refresh token의 보안적인 장점
을 둘 다 챙길 수 있다. access token은 보안 적으로 취약하니 2시간 정도로 짧게 가져가고, refresh token은 처리 비용이 많이 들기 때문에 2주 정도로 길게 가져가는 방식을 주로 사용한다.
도입 시나리오
refresh token을 프론트 서버의 session에 저장하여 프론트가 관리할 것인지, 백엔드 서버에 저장해 백엔드가 관리할 것인지가 가장 쟁점이었다.
처음에 필자는 프론트 서버의 session에 저장하게 되면 브라우저에 refresh token을 직접 저장하지 않고, session id를 저장하면 되기 때문에 session으로 관리하는 것이 더 보안상 이점이 있을 것으로 생각했으나, 어차피 session id도 탈취당하면 위험한 것은 refresh token과 마찬가지이므로 둘 중 우월한 방법은 없다고 생각한다. 팀 상황에 맞게 2가지 시나리오 중 하나를 적용하면 좋을 듯하다.
시나리오 1) refresh token을 프론트 서버의 session에 저장하는 경우
refresh token과 access token을 프론트 서버의 session에 저장해두고, 클라이언트가 token이 저장된 session에 접근하기 위해 session id를 쿠키로 가지고 있는 방식이다.
장점
-
새로 고침 상황에서 백엔드 서버까지 요청이 전달되지 않아도 된다.
프론트 서버 session에 refresh token과 access token이 사용자마다 저장되어 있기 때문에 access token을 얻기 위해 추가로 백엔드 서버로 요청을 보낼 필요가 없다. 요청 횟수에서 경제적이라 할 수 있다.
-
access token 재활용이 가능
refresh token을 백엔드 서버에 저장하는 경우는 새로 고침 했을 때, access token 만료 기간이 한참 남았음에도 access token을 재발급받는다. 왜냐하면, 프론트 서버를 거쳐 백엔드 서버까지 이미 요청이 도달했기 때문에 access token을 재활용하는 것보다 재발급하는 것이 오히려 경제적일 수 있기 때문이다. 예를 들어 만료 기간이 10초 남은 access token을 재활용해봤자 금새 access token 재발급을 위해서 백엔드 서버로 요청을 보내야 한다. 그래서 새로 고침 시에 백엔드 서버에서 그냥 token을 재발급하는 것이 오히려 더 경제적이다.
하지만 프론트 서버의 session에 refresh token과 access token을 저장하는 경우는, token을 얻기 위한 요청이 백엔드 서버 까지 전달될 필요가 없기 때문에 access token을 재활용함으로써 요청에 드는 비용을 절약할 수 있다.
단점
-
사용자가 많아질 때 session 관리가 힘들다.
사용자가 많아지게 되면 하나의 WAS를 여러개의 WAS로 나눠 사용량을 분배해야 한다고 한다. 그럼 session도 WAS마다 생겨나는데, session 정보를 요청하는 브라우저 입장에서는 이걸 모르니 WAS 앞 단인 웹서버에서 적절한 WAS의 session에 접근할 수 있도록 잘 연결해주어야 한다. 이 과정이 꽤 복잡하다고 하는데 프론트가 이를 관리하기에는 서버 지식이 깊게 필요하고, 프론트 issue도 많기 때문에 프론트가 서버 issue까지 관리하기가 힘들 것으로 판단된다.
시나리오 2) refresh token을 백엔드 서버에 저장하는 경우
refresh token을 백엔드 서버에 저장해두고 클라이언트는 refresh token에 대한 정보를 쿠키로 가지고 있는 방식이다. 새로고침했을 때 access token을 html 자원과 함께 받기 위해 반드시 백엔드 서버를 거쳐야한다.
장점
-
서버 issue를 백엔드 서버에서 대부분 처리하기 때문에 관리에 용이하다.
사용자가 많아짐에 따라 생기는 서버 issue를 백엔드에서 모두 처리하기 때문에 관리 포인트가 프론트 서버, 백엔드 서버 둘로 나뉘지 않고 백엔드 서버 한 곳에서 처리된다. 따라서 서버 issue 관리에 용이하다.
단점
-
새로 고침하면 항상 프론트 서버를 거쳐 백엔드 서버까지 요청이 전달된다.
token이 프론트 서버에 따로 저장되지 않기 때문에 access token을 얻기 위해서는 항상 백엔드 서버까지 요청이 전달되어야 한다. 요청 횟수, 요청에 드는 간접비용 등을 고려해봤을 때 비효율적일 수 있다.
고려해보면 좋을 사항
보안 - 정상적인 사용자의 ip가 아니라면 refresh token을 삭제하도록 하기
앞서 언급한 것처럼 refresh token은 서버에 저장된다. 그래서 보안적인 문제가 생기면 refresh token을 삭제함으로써 특정 사용자를 강제 로그아웃시킬 수 있다. 현재 놀토 앱은 최초 로그인 한 ip를 서버에 저장하고, refresh token을 통한 access token 갱신 요청이 다른 ip로부터 온다면 비정상적인 접근으로 판단하고 refresh token을 삭제하여 강제 로그아웃시킨다.
그런데 이 방식도 UX적으로 단점이 있다. 요즘 항상 같은 곳에서만 컴퓨터를 사용하는 유저가 얼마나 되겠는가. 지금 글을 작성하는 시점에도 많은 사람이 카페에서 노트북을 사용하고 있는데 ip가 달라진다고 해서 강제로 로그아웃시킨다면 사용자 경험에 좋지 않을 것이다.
네이버는 ip가 달라지는 경우 보안 알림을 사용자에게 띄워 준다. 여기서 “아니요” 를 선택한다면 로그아웃되는 구조로 보인다. 놀토의 다음 도전 과제는 이러한 방식이 아닐까? 언젠간 한번 도입해보고 싶은 방법이다.
UX - 사용 중에 access token이 만료된다면 조용히 재발급 받기
만약 어떤 사용자가 새로 고침하지 않고 화면을 2시간 이상 띄워놓는 경우를 가정해보자. 가장 흔한 경우는 글을 작성하는 경우가 있다. 이 경우에는 access token이 만료되었다는 사실을 사용자가 모르게 하는 것이 가장 자연스러운 경험일 것이다.
필자가 생각한 방법은 2가지가 있다. 이 또한 장단이 있으니 상황에 따라 적절히 선택하면 될 듯하다.
방법 1) 요청 보낼 때 token 만료 에러가 발생하면 token을 새로 발급받기
인가가 필요한 요청을 보낼 때 token 만료 에러가 발생하면 token을 새로 발급받고, 새로 발급받은 token으로 다시 요청을 다시 보내는 것이다.
장점
- 사용자가 인가가 필요한 작업을 요청할 때만 token이 재발급되어 경제적이다.
단점
-
인가가 필요한 요청에 대한 에러처리, 재요청 로직이 추가로 요구된다.
필자는 개인적으로 token 재발급 때문에 요청 로직이 너무 크게 변경되는 것이 부담스러워 일정 시간마다 새로운 token을 발급받는 방식을 사용했다.
-
token이 만료된 경우 인가가 필요한 요청을 결과적으로 2번 보내야한다.
token이 만료된다면 첫번째 요청은 실패할 것이고, 새로운 token을 발급받아 두번째 요청을 보내야 한다. 큰 문제가 되지는 않을테지만 단점이라 할 수 있다.
방법 2) 일정 시간마다 새로운 access token을 발급 받기
access token 만료 기간이 2시간이라면 놀토에서는 1시간 55분 정도에 새로운 token을 발급받고 있다. 이 방법을 사용하면 요청 관련 로직을 수정하지 않아도 되어 무척 편하다. 사용자가 사용하지 않을 때에도 백그라운드에서 계속 새로운 token을 발급받는다는 단점이 있을 수 있지만, 이것도 브라우저 포커스가 되어있는지를 감지해서 최적화도 가능하므로 큰 문제가 되지 않을 것으로 생각한다.
장점
- setTimeout으로 token 재발급 로직만 작성해주면 요청 관련 로직 수정이 필요 없다.
단점
- 시간과 관련하여 token을 재발급 받기 때문에 특정 시간이 지나면 무조건 재발급 요청을 보내어 비효율적일 수 있다.
마무리
오늘은 refresh token을 도입하면서 겪었던 일들을 글로 작성해보았다. 꽤 오랫동안 보안적인 측면과 UX적인 측면에서 고민을 많이 해보았는데, 지금까지 대형 서비스에서 당연하게 느끼고 있던 인가 관련 처리가 이렇게나 복잡했다니 뒤에서 묵묵히 노력하는 개발자들의 노고가 느껴졌다.
고생은 했지만 refresh token 도입이 끝나고 나니 여타 대형 서비스와 비슷한 사용자 경험을 놀토 앱이 줄 수 있다니 굉장히 만족스러웠다. 이렇게 사용자 경험적인 측면에서 보람을 느끼며 최근에는 인피니티 스크롤 최적화에도 눈길이 가는데 그것도 시간이 된다면 적용해보고 싶다. 역시 좋은 사용자 경험으로 가는 길에는 끝이 없다. 무던히 노력하자…! 😊