우리 프로젝트는 현재 JWT를 활용하고 있다.
: JSON 객체를 사용하여 정보를 안전하게 전송하기 위한 토큰
테스트 : https://jwt.io/ 에서 해볼 수 있다.
Encoded
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decoded
{"alg": "HS256", "typ": "JWT"}
{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT는 Headers, Payload, Signature로 구성되어 있다.
sub
), 사용자 이름(name
), 토큰 발급시간(iat
)로 구성되어 있다.위와 같이 헤더, 내용, 서명이 .
을 구분자로 연결되어 있다.
Token을 서버로 보내면, 서버는 JWT 라이브러리의 복호화 로직을 사용하여 서명의 Secret을 확인한다. 복호화된 데이터가 서버 DB에 저장된 토큰의 정보와 일치하는지 비교하여 판단한다.
토큰은 어디에 저장해야 안전할까, 그리고 좋은 방법일까?
이번 프로젝트는 모바일 앱에서 웹뷰를 띄우는 형태이다. 이 경우에도 Refresh Token과 Access Token을 모두 사용하여 토큰을 관리하여야 할까? 그에 앞서서, 다른 프로젝트는 어떻게 했을까?
웹뷰로 기반인 트레바리 서비스를 먼저 살펴보자.
cookie에 토큰값을 저장하고 있다. 작성일 기준 7월 3일이니, 토큰의 만료 기간은 1달임을 확인할 수 있다.
또한, cookie를 제거 후 새로고침을 하면 로그인이 풀리는 것을 확인할 수 있다. 이는 쿠키에 토큰을 저장하고 있기 때문이다.
두 번째로 최근에 PC에서도 웹뷰로 통일한 무신사 서비스를 살펴보았다.
무신사 또한 쿠키에 저장을 하고 있다. 대신, 만료 시간에 '세션'이라 적혀 있는 걸로 보아, 세션 쿠키인 것을 확인할 수 있다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
쿠키 저장소를 보면, Expires에 ‘세션’이라 적혀있는 것을 확인할 수 있다. 이 말은 즉슨, 이 쿠키는 ‘세션 쿠키’라는 것이다. 일반적인 쿠키는 expires의 시간이 지나면 제거가 되지만 세션 쿠키는 세션이 종료되면 제거가 된다는 점에서 차이가 있다.
그래서 무신사 사이트는 세션이 종료되면, 즉 브라우저가 종료되면 해당 토크니 사라지며 새로 로그인을 해야 한다.
쿠키를 저장할 때, 만료기간을 설정하면 해당 만료기간을 가진 지속 쿠키가 되고, 설정하지 않으면 세션 쿠키가 된다.
그렇다면, 다른 사이드 프로젝트에서는 어떻게 토큰을 관리하고 있을까?
디프만의 아맞다에서는 토큰을 localStorage에 저장하였다. Access Token과 Refresh Token을 따로 구분하지 않았다.
대신에, 사용자를 구분하는 userToken과 firebase 알림 토큰인 fcmToken을 또 사용하였다.
링크에서는 React Native로 구현하였는데, asyncStorage
를 사용하여 디바이스의 하드드라이브에 토큰을 저장하였다.
이 방법을 사용한다면, 로그인 > 웹에서 앱으로 토큰 전달 > asyncStorage
에 저장 > 앱에서 웹으로 필요할 때마다 토큰 저장. 이 방식으로 구현 가능하지 않을까?
디프만 - 영감탱에서는 Refresh Token과 Access Token을 모두 관리하였다.
세부적인 구현 로직은 다음과 같다.
_app.tsx
혹은 layout.tsx
페이지를 UserProvider
로 감싼다.UserProvider
에서, localStorage의 토큰을 체크한다.redirect
시킨다.위와 같은 방식으로 localStorage 혹은 asyncStroage를 이용하면 서버에서 먼저 데이터를 받아와서 페이지를 구성해서 넘겨주는 SSR 방식을 활용하지 못한다.
위와같은 Next.js의 SSR, SSG같은 장점을 활용하지 못한다는 치명적인 단점이 존재한다. 서버에서 먼저 데이터를 fetching한다는 것은 서버에서 토큰에 접근할 수 있어야 한다는 것을 의미한다. 서버에 데이터를 저장하고, 접근할 수 있는 방법으로는 쿠키가 있다.
: 사용자의 컴퓨터에 저장하는 작은 데이터 조각이다.
서버가 클라이언트에 응답할 때 쿠키에 저장하고자 하는 정보를 Header의 Set-Cookie로 함께 전달된다.
Set-Cookie: key=value; path=/;
클라이언트는 서버로 전송하는 모든 요청에, 현재 브라우저에 저장된 모든 쿠키를 Header의 Cookie로 전달한다.
Cookie: key=value; key2=value2;
document.cookie = 'key=value; path=/;'
여러 페이지를 이동할 때마다 로그인을 하지 않아도 사용자 정보를 유지할 수 있게 해주는 것이 쿠키이다.
정리하면, 쿠키는 악성적인 코드를 심는 XSS로부터 안전하고, 사용자가 HTTP 요청을 가로챈 뒤 악의적인 요청을 보낼 수 있는 CSRF로부터 위험하다. 그러므로 Refresh Token을 저장하는데 적합하다. 왜냐하면, Refreh Token만으로는 사용자의 정보를 탈취할 수 없기 때문이다. Refresh Token으로부터 Access Token을 발급받아야만 사용자의 정보를 탈취할 수 있다.
회원가입을 하거나 로그인을 했을 때, 서버로부터 Access Token과 Refresh Token, 그리고 userId를 받는다. 이를 쿠키에 저장한다.
axios를 이용하여 서버에 HTTP 요청을 할 때, headers에 Access Token을 담아서 보내야 한다. 이를 위해 axios.interceptors.request.use
를 이용하여 api요청할 때마다 headers 설정을 해주었다.
브라우저에 저장된 cookie값을 꺼낸다.
axios.interceptors.request.use를 이용하여 headers에 Access Token을 설정한다.
이번 글에서는 JWT를 활용한 토큰 기반 인증의 구조와 장단점, 그리고 Access Token과 Refresh Token의 역할에 대해 살펴보았다. 또한, 웹 및 모바일 앱 프로젝트에서 토큰을 어디에 저장할지에 대한 고민을 기반으로, 다양한 프로젝트의 사례를 통해 토큰 관리 방식을 리서치하고 비교해보았다.
JWT의 구조와 장단점:
Access Token과 Refresh Token:
토큰 저장 위치:
asyncStorage
에 저장하는 다양한 방법을 사용했다.리서치 결과:
asyncStorage
를 활용했다.최종적으로, 서버에서 브라우저에 저장된 값에 접근하기 위해서는 쿠키를 사용하는 것이 유리하며 쿠키에 저장된 값은 XSS 공격으로부터 안전하다는 장점을 가지고 있다. 그러나 CSRF 공격에 대비하기 위해 추가적인 보안 조치가 필요하다. 로컬 스토리지는 더 많은 데이터를 저장할 수 있지만, XSS 공격에 취약하다는 단점이 있다.
회원가입이나 로그인 시, 서버로부터 받은 Access Token과 Refresh Token, userId를 쿠키에 저장하고, axios를 통해 서버에 요청할 때 쿠키에서 토큰을 꺼내 headers에 설정하는 방법을 구현했다. 이를 통해 토큰 관리의 보안성과 편의성을 모두 확보할 수 있었다.
보안성, 그리고 서버에서 접근이 가능한 지를 고려하며 토큰을 저장하는 방법에 대해 고민해보았다. 브라우저 스토리지에는 쿠키, 로컬 스토리지, 세션 스토리지 등 다양한 방법이 있으며 각 장단점이 존재한다. 앞으로 개발을 할 때 이러한 부분을 고려하며 개발을 해야겠다고 생각했다.