토큰에는 두 종류
- access token (유효기간 짧은, 인가용 token)
- refresh token (유효기간 긴, access token 재발급용 token)
토큰 재발급 과정
- 로그인 인증에 성공한 클라이언트는 Refresh Token과 Access Token 두 개를 서버로부터 받는다.
- 클라이언트는 Refresh Token과 Access Token을 로컬에 저장해놓는다.
- 클라이언트는 헤더에 Access Token을 넣고 API 통신을 한다. (Authorization)
- 일정 기간이 지나 Access Token의 유효기간이 만료되었다.
- Access Token은 이제 유효하지 않으므로 권한이 없는 사용자가 된다.
- 클라이언트로부터 유효기간이 지난 Access Token을 받은 서버는 401 (Unauthorized) 에러 코드로 응답한다.
- 401를 통해 클라이언트는 invalid_token (유효기간이 만료되었음)을 알 수 있다.
- Access Token 대신 Refresh Token을 넣어 API를 재요청한다.
- Refresh Token으로 사용자의 권한을 확인한 서버는 응답쿼리 헤더에 새로운 Access Token을 넣어 응답한다.
- 만약 Refresh Token도 만료되었다면 서버는 동일하게 401 error code를 보내고, 클라이언트는 재로그인해야한다.
Access Token 재발급 API
class TokenRefreshRequestSerializer(serializers.Serializer):
refresh = serializers.CharField()
accounts/request_serializer.py 에 serializer 추가
class TokenRefreshView(APIView):
@swagger_auto_schema(
operation_id="토큰 재발급",
operation_description="access 토큰을 재발급 받습니다.",
request_body=TokenRefreshRequestSerializer,
responses={200: UserProfileSerializer},
)
def post(self, request):
refresh_token = request.data.get("refresh")
if not refresh_token:
return Response(
{"detail": "no refresh token"}, status=status.HTTP_400_BAD_REQUEST
)
try:
RefreshToken(refresh_token).verify()
except:
return Response(
{"detail": "please signin again."}, status=status.HTTP_401_UNAUTHORIZED
)
new_access_token = str(RefreshToken(refresh_token).access_token)
response = Response({"detail": "token refreshed"}, status=status.HTTP_200_OK)
response.set_cookie("access_token", value=str(new_access_token), httponly=True)
return response
- 사용자의 요청 body로부터 refresh 토큰의 값을 가져옴, 그리고 그 값이 없을 경우 400 에러를 반환
- refresh_token이 유효하지 않은 경우, ValidationError가 발생하여 except 구문이 동작하고, 401 에러를 반환
(refresh_token이 유효하지 않다는 것은 사용자가 로그아웃된 상태라는 것을 의미하기 때문)
- 마지막으로, refresh_token이 유효한 상태인 것을 확인하면 새로운 access_token을 발급하고, 이를 쿠키에 담아서 사용자에게 응답으로 보냄
from django.urls import path
from .views import SignUpView, SignInView, TokenRefreshView
app_name = 'account'
urlpatterns = [
# CBV url path
path("signup/", SignUpView.as_view()),
path("signin/", SignInView.as_view()),
path("refresh/", TokenRefreshView.as_view()),
]
accounts/urls.py 에 추가
cf) verify 함수의 내부 로직
BlacklistMixin class 내의 verify 함수의 생김새는 아래와 같다
if "rest_framework_simplejwt.token_blacklist" in settings.INSTALLED_APPS:
def verify(self, *args, **kwargs) -> None:
self.check_blacklist()
super().verify(*args, **kwargs) # type: ignore
def check_blacklist(self) -> None:
"""
Checks if this token is present in the token blacklist. Raises
`TokenError` if so.
"""
jti = self.payload[api_settings.JTI_CLAIM]
if BlacklistedToken.objects.filter(token__jti=jti).exists():
raise TokenError(_("Token is blacklisted"))
verify 함수에선 check blacklist 함수가 실행되고, 여기서 만약 건네 받은 token 이 저희 sqlite 데이터베이스 내의 blacklist 에 저장되어있으면 Token Error 가 발생
Refresh Token 보안 문제?
- 로그아웃에서 가장 중요한 로직은 바로 refresh token을 더이상 사용할 수 없도록 관리해 주는 것입니다.
- refresh token은 유효기간이 상당히 긴 편입니다. (어떤 서비스에서는 연 단위로 설정하기도 합니다…)
- 만약에 그 상태에서 누군가가 refresh token을 훔쳐가게 되면 이를 이용해서 access token을 발급받고 명의를 도용하는 등 보안상 위험한 일이 발생할 수도 있습니다.
- 그렇기 때문에 사용자가 로그아웃을 하면, 사용자가 쓰던 refresh token을 누군가가 사용할 수 없도록 안전하게 관리해야 하고, 이를 위해서 blacklist라는 것을 사용하게 됩니다
- 단순히 프론트에서 쿠키를 삭제하는 것으로만 로그아웃을 구현한다면, 로그인 당시 발급 되었던 토큰을 누군가가 탈취해서 다른 사람이 악용하고자 했을 때 백엔드는 이 유저가 로그아웃 했다는 사실을 알 수 있는 방법이 없음
- 일반적인 상황에서는 data 에 담아서 보내지 않는 refresh token 을 로그아웃을 하는 경우에 요청의 data 에 넣어서 보냄
- 백엔드에서는 전달받은 refresh token을 더 이상 사용하지 않는 BlackList에 등록!!
logout/ 이라는 URI로 refresh token을 담아 POST 요청을 보내면 대응되는 refresh token 삭제하도록 구현 ㄱㄱ
class LogoutRequestSerializer(serializers.Serializer):
refresh = serializers.CharField()
class LogoutView(APIView):
@swagger_auto_schema(
operation_id="로그아웃",
operation_description="refresh token을 삭제합니다.",
request_body=LogoutRequestSerializer,
responses={204: "No Content", 400: "Bad Request", 401: "Unauthorized"},
)
def post(self, request):
refresh_token = request.data.get("refresh")
if not refresh_token:
return Response(
{"detail": "no refresh token"}, status=status.HTTP_400_BAD_REQUEST
)
try:
RefreshToken(refresh_token).verify()
except:
return Response(
{"detail": "please signin"}, status=status.HTTP_401_UNAUTHORIZED
)
RefreshToken(refresh_token).blacklist()
return Response(status=status.HTTP_204_NO_CONTENT)
이런 API를 추가로 만들었다
'web > django' 카테고리의 다른 글
GraduArt backend 구현 중에 겪은 문제 및 trouble shooting (2) | 2024.11.06 |
---|---|
Django JWT 토큰 기반의 인증 방식 사용해 Account 관련 API 만들기 (0) | 2024.05.20 |
Django JWT 토큰 기반의 인증 방식 환경 세팅하기 (0) | 2024.05.20 |
Django Cookie, Session, 그리고 JWT (0) | 2024.05.20 |
Django User 모델과 admin page (0) | 2024.05.20 |