토큰 유효성 검사 및 재발급의 필요
앞선 포스팅에서 simple-jwt 를 이용해 회원가입, 로그인, 로그아웃 로직을 구현해보았다. (포스팅 이동)
로그인 시 발급받은 accessToken 은 보안 상 굉장히 짧은 유효기간을 가진다. myBox 프로젝트에서는 10분으로 설정해놓았었다.
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=10), # 이 부분
"REFRESH_TOKEN_LIFETIME": timedelta(hours=1),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
...
}
즉, 1) 클라이언트 요청 헤더 속 accessToken 이 만료된 토큰인지 확인해야 하는 과정이 필요하며, 2) 만료 시 쿠키에 저장된 refreshToken 정보를 통해 재발급을 해주어야 한다.
ROTATE_REFRESH_TOKENS 를 False 로 설정했기 때문에 3) access만 재발급하고 기존 refreshToken 은 건드리지 않는다.
그리고 4) 만료된 refreshToken 으로 access 재발급을 요청한다면, 401 에러를 반환하여 클라이언트에서 재로그인하도록 한다.
자, 그럼 4가지 요구사항을 구현해보자!
로직 구현
1) 클라이언트 요청 헤더 속 accessToken 이 만료된 토큰인지 확인
해당 작업은 따로 로직을 추가해줄 필요가 없다. 로그인, 로그아웃 로직을 구현하면서 settings.py 에 이미 JWT 인증을 설정해놨기 때문이다. API 요청이 오면 middleware가 만료여부를 판단할 것이다.
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
# 이 부분이다.
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
...
}
유효한 accessToken 인 경우 View 로직을 진행할 거고, 만료된 토큰이라면 401 에러를 반환한다.
2, 3) 만료 시 쿠키에 저장된 refreshToken 정보를 통해 accessToken 만 재발급
그렇다면 이젠 쿠키에 저장했던 refreshToken 을 통해 새로운 accessToken 을 발급받아야 한다.
simple-jwt 에서 역시나 기본 TokenRefreshView, TokenRefreshSerializer 를 제공한다.
View
TokenRefreshView 를 보면 로그인포스팅에서 보았던 TokenObtainPairView 등과 동일하게 TokenViewBase 를 상속하고 있다.
그럼 뭐다? TokenViewBase의 post 메서드를 오버라이딩하자!
class TokenRefreshView(TokenViewBase):
"""
Takes a refresh type JSON web token and returns an access type JSON web
token if the refresh token is valid.
"""
_serializer_class = api_settings.TOKEN_REFRESH_SERIALIZER
token_refresh = TokenRefreshView.as_view()
JWTRefreshView 를 만들고 TokenRefreshView 를 상속했다.
serializer_class 에는 밑에서 구현할 JWTRefreshSerializer 를 주었고,
쿠키의 refreshToken 정보를 serializer 에게 주고, JsonResponse 를 반환하도록 post 메서드를 오버라이드했다.
class JWTRefreshView(TokenRefreshView):
serializer_class = JWTRefreshSerializer
def post(self, request: Request, *args, **kwargs) -> JsonResponse:
token_data = {"refresh": request.COOKIES.get("refresh", "")}
serializer = self.get_serializer(data=token_data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
json_response = JsonResponse(
{
"code": status.HTTP_200_OK,
"message": "new accessToken",
"token": {"access": serializer.validated_data["access"]},
},
status=status.HTTP_200_OK,
)
return json_response
Serializer
TokenRefreshSerializer 는 refresh 를 body 로 받아 새로운 accessToken 을 만들고 refresh, access 토큰 정보를 반환한다.
이 때 ROTATE_REFRESH_TOKENS 가 True 이면 refresh 토큰 또한 재발급한다. 이때 BLACKLIST_AFTER_ROTATION 가 True 이면 기존 refreshToken 을 블랙리스트 처리한다.
하지만 이번 프로젝트 세팅은 둘다 False 이므로 access 토큰만 재발급한다.
class TokenRefreshSerializer(serializers.Serializer):
refresh = serializers.CharField()
access = serializers.CharField(read_only=True)
token_class = RefreshToken
def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]:
refresh = self.token_class(attrs["refresh"])
data = {"access": str(refresh.access_token)}
if api_settings.ROTATE_REFRESH_TOKENS:
if api_settings.BLACKLIST_AFTER_ROTATION:
try:
# Attempt to blacklist the given refresh token
refresh.blacklist()
except AttributeError:
# If blacklist app not installed, `blacklist` method will
# not be present
pass
refresh.set_jti()
refresh.set_exp()
refresh.set_iat()
data["refresh"] = str(refresh)
return data
JWTRefreshView 는 TokenRefreshView 를 상속해 만들었다.
로그아웃View 인 JWTLogoutView 와 마찬가지로 refreshToken 을 쿠키에서 가져오기 때문에 refresh 값에 대한 required 를 False 로 변경했다.
class JWTRefreshSerializer(TokenRefreshSerializer):
refresh = serializers.CharField(required=False, write_only=True)
urls.py
refresh 관련된 url 을 추가해주자
urlpatterns = [
path("signup/", SignUpView.as_view(), name="signup"),
path("login/", JWTLoginView.as_view(), name="login"),
path("logout/", JWTLogoutView.as_view(), name="logout"),
path("token/refresh/", JWTRefreshView.as_view(), name="token_refresh"), # 추가
]
테스트
drf-spectacular 를 적용했기 때문에 요청이 가능한 API docs 가 있다. 그걸 이용해 테스트할거다.
테스트를 위해 우선 settings.py 에서 accessToken 과 refreshToken 의 유효기간을 짧게 조정해주었다.
SIMPLE_JWT = {
# "ACCESS_TOKEN_LIFETIME": timedelta(minutes=10),
# "REFRESH_TOKEN_LIFETIME": timedelta(hours=1),
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=1),
"REFRESH_TOKEN_LIFETIME": timedelta(minutes=5),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
...
}
먼저, api/v1/schema/swagger-ui 로 접속해 로그인 요청하고 access, refresh 토큰을 얻어보자
캡쳐는 못했지만 쿠키에 refreshToken 이 저장되어 있다.
이제 얻은 accessToken 을 복사하여 우측 상단의 Authorize 버튼을 눌러 인증을 진행하자
캡쳐의 하단을 보면 쿠키에 refresh 정보가 저장되어 있다. 이를 이용해 새로운 accessToken 을 요청했다.
200 코드와 함께 정상적으로 새로운 accessToken 이 응답에 포함되어 있음을 확인할 수 있다.
만료된 refreshToken 으로 refresh 요청을 한다면, 아래처럼 401 에러를 반환한다. 이를 보고 클라이언트는 로그인 화면으로 사용자를 이동시켜야 한다.
이 때 만료된 쿠키에 대해서는 클라이언트 쪽에서 삭제하거나 다른 방식으로 관리할테니 서버 측에서는 blacklist 처리할 필요가 없다.