오늘은 Django REST Framework (이하 DRF) 와 서드파티 패키지인 simple_jwt를 활용하여 인증하는 방법을 다뤄보려고 한다.
Postman을 활용하여 백엔드 코드를 구현할 때는 잘 되던 django.contrib.auth에 있는 로그인함수를 통한 로그인이 프론트엔드에서 fetch를 통해서 보낸 데이터를 인증하고 세션에 저장하지 못한다는 걸 발견했다.
로그인 함수를 통과해서 status 200까지 리턴을 해 놓고는 막상 request.user를 조회하려고 하니 Anonymous user 가 나오고 지금 유저가 로그인 상태인지 아닌지 검사하는게 사실상 불가능했다
그래서 DRF 공식문서를 찾아보니 아래와 같이 기존 장고에서의 csrftoken을 활용과 DRF에서의 활용방법이 상이한 면이 있고 로그인에 사용하기에는 무리가 있다고 써져 있었고
CSRF validation in REST framework works slightly differently from standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied.
비교전 간단하게 auth를 구현할 수 있는 것 같은 basic auth 또한 아래의 글과 같이 http's' 에서만 인증기능을 사용할 수 있다고 해놓았다.
Note: If you use BasicAuthentication in production you must ensure that your API is only available over https. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
다음으로 DRF는 token auth 방식 또한 제공하고 있었다. 그러나 이 token auth 방식은 유효시간 설정을 위해서 REST Knox라는 서드파티 패키지를 불러와서 따로 작업을 해줘야 하고 고정된 토큰값을 db에 저장하고 그 값으로 인증을 하는 절차는 지금 빠르게 연습해보고자하는 나에게는 맞지 않는 것 같아 payload를 통해서 유효기간 설정도 가능하고 token안에 다양한 값들을 넣어 보관할 수 있는 jwt토큰이 나에게 제일 맞는 방식이라고 생각되어 jwt토큰인증 방식을 알아보았다.
검색을 통해서 찾아낸 서드파티 패키지인 rest_framework_jwt를 다운받고 공식문서에서 시키는대로 작업을 진행하던 도중
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
👉'rest_framework_jwt.authentication.JSONWebTokenAuthentication',👈
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
위 코드에서 JSONWebTokenAuthentication을 찾을 수 없다는 에러가 났다. 내가 뭘 잘못한건지 몰라서 별별 설정을 다 만져가면서 커스텀 해보고 바꾸어봐도 계속해서 같은 에러가 났다. 에러를 통해 구글링을 해보니 stackoverflow에서 jwt 패키지가 더이상 유지되지 않아 4.0 이상 버전에는 호환이 되지 않는다는 사실을 발견했다.
심지어 DRF 공식 홈페이지에서도 simple_jwt패키지를 소개하고 있었다. 나는 도대체 무슨 삽질을 한건가 버전체크나 버전에 따른 호환 검사는 기본이고 공식문서에서도 다른 패키지를 권하고 있는데 오래전에 올린 블로그 글 하나만 믿고 이 패키지를 사용해 많은 시간을 날려먹었다.
그래서 결국 돌고돌아 결국 simple_jwt로 구현을 시작했다.
simple_jwt는 말그대로 정말 심플하다. 제공하는 문서에도 너무 심플하게 나와있어 다 설명을 안했다고 생각했으나 말 그대로 설명할 게 별로 없고 이미 다 구현해놨기 때문에 그랬다.
pip install djangorestframework-simplejwt
INSTALLED_APPS = [
...
'rest_framework_simplejwt',
...
]
우선 pip를 통해 simplejwt를 다운받고 내 프로젝트 폴더에 있는 settings.py 인스톨드앱에 추가해준다.
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
...
👉'rest_framework_simplejwt.authentication.JWTAuthentication',👈
)
...
}
settings.py에 REST_FRAMEWORK DEFAULT_AUTHENTICATION_CLASSES 에 다음과 같은 코드도 추가해준다. 이 코드는 auth 방식에 jwt토큰 인증을 추가하겠다는 코드다
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
...
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
...
]
위 코드를 urls.py 에 추가해준다. 물론 url은 마음대로 설정이 가능하다.(나는 /user/login 으로 설정해줬다) 위와 같이 url을 설정해두고 내 db에 저장 돼 있는 유저의 아이다값과 패스워드값을 넘겨주면 access, refresh 에 각각 토큰을 담아서 보내준다.
주어진 값들을 localstorage에 저장해두고 인증이 필요한 유저활동마다 access에 자기가 설정한 인증방식대로 넣어주면 된다.
JWT의 유효기간 설정이나 시크릿키 설정은 settings.py에 SIMPLE_JWT를 추가해주면 간단하게 제어할 수 있다.
아래는 심플 jwt의 공식문서에서 제공하는 설정을 토대로 내가 필요한 값만 가져와 고친 코드이다.
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'SIGNING_KEY': configure.SECRET_KEY,
}
위 세 값만 제어해주고 나머지는 디폴트로 설정했다. 추후에 더 공부를 통해 refresh 기능을 활용해보려고 하지만 지금은 딱 기능적인 부분 구현만 필요하므로 생략하겠다.
이제 다 끝이 났다.
async function login() {
const user_email = document.getElementById('email').value;
const user_password = document.getElementById('password').value;
if (user_email && user_password) {
// 미리 변경해둔 url로 요청을 보냄
fetch(BASE_URL + '/user/login', {
method: "POST",
mode: 'cors',
headers: {
"Access-Control-Allow-Origin": "*",
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken,
},
body: JSON.stringify({
"email": user_email,
"password": user_password
}),
}).then(res => res.json())
.then(data => {
// 쿠키에 저장해서 주고 받는 것보다 로컬스토리지에 저장하는 것이 더 나아보여서 로컬스토리지에 저장함
for (const key in data) {
localStorage.setItem(key, data[key])
}
location.replace('/post/main.html')
})
.catch(error => {
alert(error)
})
} else {
alert("아이디와 패스워드를 입력해주세요")
}
}
위와 같이 fetch 함수를 통해 토큰을 받아올 url(정확히는 아까 설정해둔 TokenObtainPairView라는 클래스)에 post 요청을 보내면 javascript object 형태로 토큰을 보내준다.
for (const key in data) {
localStorage.setItem(key, data[key])
}
받아온 오브젝트의 키값을 통해 for loop를 돌리고 각 데이터와 키 값을 매치시켜 로컬 스토리지에 저장해준다.
const token = localStorage.getItem('access')
headers: {
"Access-Control-Allow-Origin": "*",
// simple_jwt에서 제공하는 인증헤더의 default값이 Bearer이므로 아래와 같이 헤더에 담아줌
"Authorization": `Bearer ${token}`,
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken,
},
로컬스토리지에 저장된 값은 getItem을 통해 불러올 수 있다. 'access'라는 키 값으로 토큰은 저장되어 있다.
'AUTH_HEADER_TYPES': ('Bearer',),
simple_jwt에서 default로 설정한 auth header type은 bearer다.
"Authorization": `Bearer ${token}`
그래서 ` 백틱에 유의하여 위와같이 넣어준다.
이제 마지막으로 토큰을 받아 로그인을 완료했으니 인증받은 사용자들이 데이터 조회나 생성을 할 수 있게 해주고 인증되지 않은 사용자들은 걸러주는 permission 설정을 해줄 차례이다. 다시 백엔드 코드로 돌아와서 jwtauthentication을 import 해주고 인증이 필요한 View클래스 아래에 위와 같이 각 클래스들을 명시해주면 인증이 된 사용자 그러니까 access토큰을 가진 사용자만 통과되어 데이터에 접근할 수 있게 된다.
from rest_framework_simplejwt.authentication import JWTAuthentication
class PostView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
cur_user = request.user
request.data['author'] = cur_user
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
######## 커스텀 가능 ###############
token['name'] = user.name
ex. token['nickname'] = user.nickname
################################
return token
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
내 views.py에 위 코드를 가져와 토큰에 내가 원하는 값들을 넣어줄 수 있다. 그러면 javascript에서 아래와 같은 코드를 사용하여 jwt토큰을 decode할 수 있고 이를 이용해서 닉네임 값만 가져와 지금 로그인한 사용자가 어떤 닉네임을 가졌는지만 간단하게 나타낼 수도 있다.
const parseJwt = (token) => {
try {
return JSON.parse(atob(token.split('.')[1]));
} catch (e) {
return null;
}
};
https://stackoverflow.com/questions/72102911/could-not-import-rest-framework-jwt-authentication-jsonwebtokenauthentication : jwt_auth가 더이상 유지되지 않는다고 알려주는 스택오버플로
https://www.youtube.com/watch?v=xjMP0hspNLE : react와 django를 통해 simple_jwt를 구현해봄 제일 도움이 많이됨!!
https://www.youtube.com/watch?v=fXOKBbnMQow : authenticate 방법에 대한 인사이트를 얻음
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/for...in : 자바스크립트에서 for .. in 사용하는법
https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library : jwt디코드 코드
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html : simple_jwt공식문서의 설정페이지
https://www.django-rest-framework.org/api-guide/authentication/ : DRF authentication 설명 페이지
공식문서까지 참고하셔서 custom 기능까지 잘 적용해보셨네요 !