JWT 찍먹하고 DRF에서 사용해보기

Hyemi Noh
10 min readSep 25, 2021

--

개인 프로젝트에 JWT를 도입해보면서 이해한 점들을 정리하려고 한다.

들어가기 전에

이 글은 JWT를 처음 보는 사람들을 위한 글이다. 따라서 심화된 내용을 원하는 사람들은 다른 글을 보는 걸 추천한다. JWT를 처음 봤을 때 잘 이해가 되지 않았던 것 위주로 정리해놨다. 개인 프로젝트에 django restframework를 이용했으므로 코드 예시는 django restframework로 한다.

목 차

- JWT란?
- JWT의 역할
- Token의 종류와 expiration
- 다른 방식과 비교 (session, drf token)
- 대칭키 vs 비대칭키
- 블랙리스트

JWT란?

JWT는 Json Web Token의 줄임말이다. 간단하게 설명하면 서버가 시크릿 키와 데이터를 이용해 고유한 문자열(signature)을 생성하고 이걸 통해 클라이언트의 권한을 검증한다.

JWT는 header, payload, signature 세 가지로 이루어져 있다. 각각에 대한 설명은 zz3n.logJWT 구조 부분을 보면 알 수 있다. 만약 감이 잘 안 오면 NHN Cloud Meetup구조와 생성 부분도 봐보자. 이미 잘 정리된 부분이어서 다시 정리는 하지 않겠다 😅 단 , zz3n.log 블로그 글을 보면 “Base64로 암호화”돼있다고 나오는데 좀 더 정확하게 하면 “Base64 URL-Safe로 인코딩”됐다고 해야한다. Base64 URL-Safe는 Base64 인코딩에서 +-로, /_로 대체된 인코딩 방법이라고 한다.

JWT의 역할

JWT는 authorization을 위한 것이다. JWT를 이용해 요청을 보내는 유저가 권한이 있는 올바른 유저인지 확인하는 것이지 username과 password를 통해 등록된 유저인지 확인하는 게 아니기 때문이다. 물론 토큰을 발급받기 위해 처음에 유저 정보를 넘겨주기 때문에 헷갈릴 수 있다. 그러나 코드를 살펴보면 JWT는 해당 정보를 받아서 다른 수단을 이용해 authentication을 한다. 혹시 아직도 헷갈린다면 Training Outputs를 봐보자.

JWT Token을 얻기 위해 POST로 유저 정보를 넘겨줘야함.

그리고 JWT를 쓰면 인코딩을 하니 뭔지 모르게 안전한(?) 기분이 들어 HTTPS를 안 써도 되는게 아닐까 생각할 수 있다. 하지만 authentication 정보를 안전하게 넘겨주기 위해서 HTTPS는 반드시 필요하다. JWT가 모든 걸 해결해주는 만능은 아니다.

Token의 종류와 expiration

클라이언트가 JWT를 발급하는 API로 유저 정보를 body에 담아서 보내면 서버는 access token과 refresh token을 발급해준다. access token은 authorization이 필요한 API를 사용할 때마다 헤더의 AuthorizationBearer {access token} 로 보내면 된다. 꼭 Bearer로 해야하는 건 아니고 고칠 수 있지만 마땅한 이유가 없다면 원래 설정한 대로 하는 게 좋은 것 같다.

access token은 expiration time이 존재한다. 즉, 일정 시간이 지나면 사용하지 못한다. 하지만 클라이언트가 expiration time이후에도 authorization이 필요할 수 있지 않은가? 그래서 refresh token이 존재한다. JWT를 refresh 하는 API로 JWT를 발급할 때 받은 refresh token을 body로 넘겨주면새로운 access token을 발급받을 수 있다.

expiration time이 존재하는 이유는 해당 토큰이 누군가에게 털릴 가능성이 있기 때문이다. 만약 expiration time이 존재하지 않을 때, 누군가가 악의적 의도로 토큰을 털어버리면 보안상 심각한 이슈가 될 수 있다. 언제든지 사용 가능한 만능 토큰이 돼버린다 😓

다른 방식과 비교

Session vs JWT

session도 역시 JWT와 비슷하게 서버에서 session ID라는 것을 발급하고 authorization에 이용한다. 하지만, session은 검증을 위한 정보를 서버의 DB에 저장하고 있기 때문에 DB에 빈번하게 요청을 해야한다. JWT는 검증을 위해 별도로 DB에 요청하지 않는다. 클라이언트가 보내는 JWT에 정보가 이미 다 있기 때문에 서버는 자신이 가진 검증 키를 이용하기만 하면 된다. 그랩의 블로그의 그림을 보면 무슨 의미인지 알 것이다.

drf Token vs JWT

다른 프레임 워크는 모르겠으나 drf(django restframework) 같은 경우 기본적으로 제공하는 TokenAuthentication이 있다. 해당 인증 역시 토큰 기반인 것은 JWT와 같다. 하지만, expiration이 존재하지 않는다. 또한 검증을 위해 DB에 접근해야한다.

대칭키 vs 비대칭키

대칭키(symmetric key)는 키 하나로 복호화와 암호화를 하는 것이고 비대칭키(asymmetric key)는 복호화와 암호화를 서로 다른 키로 하는 것이다. 조금 더 자세하게 말하면 대칭키는 시크릿 키를 이용하는 것이고 비대칭키는 시크릿 키와 퍼블릭 키를 이용하는 것이다. drf의 JWT third-party package인 djangorestframework-simplejwt 에서는 기본적으로 대칭키를 이용한다. 즉, 시크릿 키 하나로 signature를 만들어내고 검증을 한다.

하지만 이럴 경우 시크릿 키가 더 이상 시크릿 키가 아니게 된다. JWT를 이용해 authorization을 해야하는 모든 어플리케이션이 이 키를 알고있어야 하기 때문이다. 따라서 프로덕션 환경에서는 대칭키보다는 비대칭키를 이용하는 게 더 안전하다. 비대칭키를 이용하면 시크릿 키는 신뢰할만한 곳에서만 사용해 signature를 만들어내면 되고 검증이 필요한 어플리케이션에서는 퍼블릭 키만 이용하면 된다.

대칭키이냐 비대칭키이냐에 따라 검증 과정이 달라지는데 이건 The Hard Parts of JWT Security Nobody Talks AboutHow it works부분을 참고하면 된다. 결국 두 방식의 핵심적 차이는 위에서 언급한 것처럼 어플리케이션이 시크릿 키를 공유하냐 그렇지 않냐이다.

블랙리스트

JWT는 expiration time을 가지고 있다. 하지만 클라이언트가 로그아웃을 해서 더 이상 refresh token을 사용하지 않더라도, expiration time까지는 토큰이 유효하다. refresh token은 보통 expiration time을 길게 잡기 때문에 중간에 토큰이 탈취돼 악용될 위험이 있다.

실제로 별도의 설정을 하지 않고 expiration time만 설정해놓으면, expiration time이 지나기 전에 이전에 발급한 refresh token을 재사용할 수 있다. 즉, 이 토큰을 이용해 access token을 마음껏 refresh 해서 사용할 수 있다는 의미이다. 따라서 expiration과 상관없이 토큰이 더 이상 valid 하지 않도록 하는 방법이 필요하다. 이게 바로 블랙리스트이다.

blacklist 처리한 refresh token은 더 이상 사용할 수 없음.

로그아웃 혹은 refresh token이 탈취돼 invalid 해야할 때 blacklist api에 요청을 해서 해당 refresh token을 invalid 시킨다. (djangorestframework-simplejwt는 포스트를 작성한 시점에서는 blacklist를 위한 별도의 view를 제공하지 않으므로 직접 구현해야한다. 여기를 참고했다.) 이렇게 하면 더 이상 refresh token을 사용할 수 없다. 단, access token은 expiration time이 비교적 짧기 때문에 보통 invalid에 포함하지 않는 것 같다. 일단 drf에서는 안 된다.

원리

유저 정보를 이용해 access token과 refresh token을 발급받을 때마다 특정 테이블에 “token”, “만료 시간”, “user id”, “jti”(token id)를 기록한다. blacklist api를 통해 refresh token을 블랙리스트에 등록할 때마다 블랙 리스트를 기록하는 테이블에 “블랙리스트가 된 시간”, “token id”를 기록한다.

token_blacklist_outstandingtoken table
token_blacklist_blacklistedtoken table

블랙 리스트를 기록할 때는 request의 payload에 있는 해당 토큰의 jti를 lookup field로 이용해 원하는 토큰을 얻어낸 후, 블랙리스트 토큰을 만들어낸다. jti는 jwt의 payload에 있다. 아래는 djangorestframework-simplejwt의 blacklist 함수이다. 함수를 읽어보면 더 정확하게 알 수 있을 것이다.

def blacklist(self):
jti = self.payload[api_settings.JTI_CLAIM]
exp = self.payload['exp']
# Ensure outstanding token exists with given jti
token, _ = OutstandingToken.objects.get_or_create(
jti=jti,
defaults={
'token': str(self),
'expires_at': datetime_from_epoch(exp),
},
)
return BlacklistedToken.objects.get_or_create(token=token)

--

--