Pt. 3) 토큰 저장하기
파트 2에서는 프론트엔드에서 로그인 성공 여부를 확인하고 토큰을 받아오는 데에 성공했다. 토큰은 서버가 저장하는 것이 아니라, 클라이언트가 저장했다가 서버에게 보내면 서버는 확인만 해 준다고 배웠다. 그러므로 이번에는 클라이언트가 토큰을 어떻게 저장하는지 알아보고, 그 중 하나를 선택해서 직접 실행해 보자.
JWT는 어디에 저장해야 하는가?
토큰을 받으면 어디에 저장해야 할까? 먼저, JavaScript 내부 변수에 저장하면 어떻게 될지 생각해 보자. JavaScript가 저장하는 변수는 휘발성이기 때문에 웹 페이지를 새로고침하는 순간 사라지게 된다. 토큰을 가지고 있으면 액세스 정보를 저장할 수 있다는 것이 한 가지 장점인데, 새로고침할 때마다 또는 이동할 때마다 액세스 정보가 사라져서 새로 로그인해야 한다면 토큰을 저장하는 이유가 없을 것이다.
그렇다면 브라우저 내부 저장 수단을 떠올릴 수 있다. 대표적으로 떠올릴 수 있는 두 가지가 localStorage와 쿠키이다. localStorage는 JavaScript 코드 상에서 접근하기 쉽지만 그만큼 JavaScript를 통한 XSS 공격을 통해 토큰이 탈취될 가능성이 높다. 토큰은 가지고 있는 것만으로도 액세스가 허용되는 일종의 개인 정보와 같은 것이기 때문에 탈취된다면 큰 위협을 맞닥뜨릴 가능성이 있다.
그러면 쿠키를 사용했을 때에는 보안이 완벽한가? 물론 그것은 아니다(애초에 100% security란 존재할 수 없다). 또한, HTTP 요청 헤더에 쿠키가 포함된다는 점을 이용하여 중간에 개입하여 요청을 조작하는 등 쿠키도 취약점이 존재하는 방법임은 틀림없는 것 같다.
이번 JWT 구현에서는 쿠키를 사용해서 저장하고자 하지만, 보안을 위해서는 좀 더 손을 보는 방법이 존재하는 것 같다. 기회가 된다면 해당 프로젝트에서 토큰 저장 후 검증, 갱신까지 마친 후에 보안 관련해서 토큰 저장 코드를 리팩토링하는 것도 좋아 보인다. 일단은 브라우저가 토큰을 저장해서 백엔드와 통신하도록 만드는 것이 최종 목표이기 때문에, 하나의 방법을 선택하는 것이 좋아 보였고 결과 쿠키를 선택하게 되었다.
(외부 참조) JWT를 조금 더 안전하게 저장하기 & 쿠키와 웹 스토리지 https://prolog.techcourse.co.kr/studylogs/2272
이외에도 구글에 "JWT 저장"이라는 키워드로 검색하면 양질의 글들이 많이 나오니 흥미가 있는 사람은 찾아 보기 바란다.
쿠키에 저장하기 위해서는 프론트엔드 코드를 바꿔야 할까?
그럼 쿠키에 저장하기로 했으니, 한 가지 의문점이 생긴다. 쿠키에 저장하기 위해서는 프론트엔드 코드에 접근해야 하는지에 관한 것이다. 당연히 프론트엔드의 코드를 건드리는 걸로도 쿠키에 접근할 수 있다. universal-cookie나 react-cookie 등 쿠키 액세스와 관련된 라이브러리를 통해 브라우저에 직접 쿠키를 넣어 주고 상세 설정을 관리할 수 있다.
그러나, 쿠키 저장은 반드시 프론트엔드 코드를 통해서만 수행되는 것은 아니다. 백엔드에서 응답을 생성할 때, 응답 헤더에 Set-Cookie를 지정해 주는 경우에도 이를 수신한 브라우저가 쿠키를 저장할 수 있다.
위와 같이 방법은 다양하지만, 이번에는 백엔드에서 응답할 때 Set-Cookie를 지정해서 브라우저에게 쿠키를 저장하도록 하는 방법을 사용해 보자.
헤더에 Set-Cookie 지정하기
user/views.py
응답 헤더를 추가하기 위해서는 생성했던 view의 post 메소드를 오버라이딩해야 한다.
from rest_framework import status
from rest_framework.response import Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
...
class MyTokenObtainPairView(TokenObtainPairView):
...
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
# 여기부터 추가 내용
res = Response(serializer.validated_data, status=status.HTTP_200_OK)
access = serializer.validated_data["access"]
refresh = serializer.validated_data["refresh"]
res.set_cookie("access_token", access, max_age=60 * 5, httponly=True)
res.set_cookie("refresh_token", refresh, max_age=60 * 60 * 24 * 14, httponly=True)
return res
추가 내용이라고 표시된 부분 이전은 TokenObtainPairView에서 상속받은 TokenViewBase의 post 메소드와 같은 내용이다. 중요한 것은, 원래는 Response를 생성하여 바로 반환해주었던 것에 반해 Response를 생성한 후 access와 refresh를 받아와 res의 set_cookie를 통해 Set-Cookie 헤더를 추가해 준다는 것이다.
이때, max_age 옵션은 초 단위로 지정되기 때문에 우리가 이전에 지정해 줬던 설정과 같이 access_token은 5분, refresh_token은 14일로 지정하도록 하자. 이후 응답을 반환하면 된다.
테스트하기
직접 프론트엔드 페이지를 띄워 로그인했을 때 쿠키를 확인해 보자. 개발자 도구를 띄워서 확인해 보면 아래와 같이 쿠키가 응답에 담겨서 오는 것을 확인할 수 있다.
토큰의 값을 포함한 만료 기간, httpOnly 속성 역시 잘 전달되었음을 확인할 수 있다.
마치며
사실 JWT를 저장하는 것에 관해서 localStorage에 저장하는 것이 좋다, 쿠키에 저장하는 것이 좋다, 토이 프로젝트 수준에서는 보안을 크게 신경 쓰지 않아도 된다(?) 등 다양한 의견을 찾아 볼 수 있었기 때문에 뭔가 뜬구름 잡는 소리를 늘어놓은 감이 없지 않아 있는 것 같다.
프론트엔드 개발을 진행하면서 대부분 구글링을 통해 명확한 답을 찾을 수 있었던 것에 비해 JWT 저장에 관해서는 뭔가 확실한 답을 찾을 수 없는 것 같아서 살짝 답답하기도 했는데, 실무에서 사용할 때에는 보안 이슈를 해결하기 위해 다양한 방법을 동원한다고 하니, 생각해 보면 순수하게 JWT를 통해서만 구현하려고 하면 의견이 갈리는 것도 이해가 안 되지는 않는다.
언젠가 한번 꼭 간단하게나마 보안 이슈를 해결해 보고 회고하는 글을 작성했으면 좋겠다.
'공부 > Django' 카테고리의 다른 글
Django + React로 로그인 시스템 구현하기 / Pt. 1) Django JWT 설정하기 (0) | 2022.10.17 |
---|---|
Django 초기 설정 - 가상 환경부터 프로젝트 생성까지 (0) | 2022.10.13 |