DRF 개발 일기 - 2
인증방식 변경하기
by HOON
Last updated on May 5, 2024, 5:07 p.m.
Session to JWT
이전 게시글에서 작성한 DRF를 사용한 좋아요 기능을 구현하다가 한가지 의문점이 들었습니다.
"좋아요를 누를 때 사용자 인증과정이 어떻게 되는거지?"
개요
장고는 기본적으로 인증과정을 session을 사용하여 인증합니다.
즉, 사용자의 인증정보를 서버(django)가 갖고있습니다.
이는 간단하고 안전하지만, 사용자가 많아질수록 서버의 부담이 커집니다.
따라서 저는 사용자 인증정보를 클라이언트가 가지고있는 jwt(json web token) 으로 인증방식을 변경하기로 했습니다.
이 토큰을 사용하게되면, 클라이언트는 서버측으로 PUT,POST,PATCH 등 요청시 헤더 내 자신이 갖고있는 고유키값을 같이 전달하여 서버로부터 인증을 받는 방식입니다.
구현
우선 현재 제 환경은 장고의 기본 auth를 사용중입니다.
따라서 로그인, 회원가입 등 기본적인 view와 기능은 이미 구현이 된 상태로 사용중이였습니다.
jwt를 구현하기위해선 이 부분을 custom 할 필요가 있습니다.
전체적인 흐름은 아래와 같습니다.
사용자의 로그인/회원가입 -> 서버에서 사용자에게 access/refresh token 발급 -> 해당 토큰으로 서버에 api요청시 헤더에 토큰 삽입하여 요청 -> 서버는 해당 요청에 토큰값이 유효한지 확인 후 api에 대한 응답 제공
우선 drf의 jwt token을 사용하기위하여 settings.py를 업데이트 하겠습니다.
INSTALLED_APPS = [
. . .
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
'SLIDING_TOKEN_LIFETIME': timedelta(days=30),
'SLIDING_TOKEN_REFRESH_LIFETIME_LATE_USER': timedelta(days=1),
'SLIDING_TOKEN_LIFETIME_LATE_USER': timedelta(days=30),
'BLACKLIST_AFTER_ROTATION':True,
}
각 코드의 설명을 드리자면
INSTALLED_APPS : django에서 사용하는 jwt는 simplejwt가 있습니다. 또한 blacklist는 아래에 추가로 설명 할 예정이니 일단 추가해보겠습니다.
REST_FRAMEWORK : DEFAULT_AUTHENTICATION_CLASSES 이름에서 유추 가능한것처럼 기본 인증방식을, JWTAuthentication으로 변경 하겠다는 뜻입니다.
SIMPLE_JWT : simplejwt를 사용하면서 설정 할 수 있는 옵션값입니다. access token의 유효기간 등을 설정 할 수 있습니다.
그럼 다시 설명으로 돌아가서,
사용자의 로그인/회원가입
이 부분에서는 간단하게 사용자의 로그인화면, 회원가입 화면을 구현해보겠습니다.
우선 login.html을 보자면, 간단하게 textfield로 id,pwd를 입력받아 "로그인" 버튼이 클릭 될 때 jsp를 사용하여 토큰을 생성하도록 했습니다.
<script>
document.querySelector('form').addEventListener('submit', function(event) {
event.preventDefault();
var username = document.querySelector('input[name="username"]').value;
var password = document.querySelector('input[name="password"]').value;
fetch('/api/token/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({username: username, password: password})
})
.then(response => response.json())
.then(data => {
// JWT 토큰을 로컬 스토리지에 저장합니다.
localStorage.setItem('jwt_token', data.access);
localStorage.setItem('refresh_token', data.refresh);
event.target.submit();
})
.catch((error) => {
console.error('Error:', error);
});
});
</script>
해당 버튼이 클릭되면, api/token 가 실행되며, 토큰을 생성한다는 뜻이고, 해당 토큰을 localStorage에 저장하겠다는 뜻입니다.
여기서 api/token은 simplejwt에서 제공되는 뷰를 사용하였습니다.
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/',TokenRefreshView.as_view(),name='token_refresh'),
path('api/token/blacklist/', TokenBlacklistView.as_view(), name='token_blacklist'),
따라서 api/token으로 api가 실행되면 TokenObtainPairView가 실행되며, accessToken이 생성됩니다.
마찬가지로 blacklist 토큰도 생성되지만 이는 특정상황에서 생성됩니다. 아래에서 설명하겠습니다.
서버에서 사용자에게 access/refresh token 발급
실제로 로그인시 token이 생성되는지 살펴보겠습니다.
위 스크린샷은 실제로 api/token 화면에서, id,pwd를 입력 후 POST 요청을 보냈을때의 화면입니다.
refresh와 access 토큰 모두 정상적으로 생성 된 것을 확인 할 수 있습니다.
토큰을 주고받는 과정 살펴보기
우선 사용자가 로그인이 된 상태에서 LocalStorage를 살펴보면, /api/token/ 에 의해 토큰이 발급된 상태입니다.
이 상태에서 사용자와 서버는 access token을 활용하여, 서버에 요청, 클라이언트로 응답을 거치게 됩니다.
실제로 "좋아요" 표시를 눌러 어떻게 전달되는지 확인해보겠습니다.
현재 좋아요 버튼은 클릭하게되면, drf를 이용해 api로 POST 요청을 보내게되고, 서버는 해당 사용자가 인증되어있는 사용자인지 검증 이후 count를 증가시키는 과정입니다.
먼저, 버튼이 클릭 될 때의 script를 보겠습니다.
<script>
document.getElementById('like-btn').addEventListener('click', function(event) {
// 기본 동작을 취소합니다(페이지 이동 방지).
event.preventDefault();
//로그인 여부 확인
if (!'{{ request.user.is_authenticated }}'){
return;
}
var csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
//var jwt_token = '{{ jwt_token }}';
var jwt_token = localStorage.getItem('jwt_token');
fetch('/blog/{{ post.id }}/like/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 인증 토큰을 추가합니다.
'X-CSRFToken': csrftoken,
'Authorization': 'Bearer ' + jwt_token
},
body: JSON.stringify({id: '{{ post.id }}'})
})
.then(response => {
if (!response.ok){
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// '좋아요'의 수를 업데이트합니다.
document.getElementById('like-count').textContent = data.likes;
})
.catch(error => {
console.error('There has been a problem with your fetch operation:',error);
});
});
</script>
먼저 request.user.is_authenticated
로 사용자가 로그인되어있는지 우선 확인합니다.
로그인이 되어있지않을경우는 false로 아무런 작동을 하지 않습니다.
이후, var jwt_token = localStorage.getItem('jwt_token');
로 jwt_token을 클라이언트의 localStorage에서 가져옵니다.
다음, header
구문에 Authorization을 jwt토큰값을 입력해줍니다. 이 때 저는 jwt 토큰을 사용하고있기때문에, 토큰을 소유한 사람에게 액세스 권한을 부여하는 일반적인 토큰 클래스 "Bearer"을 먼저 작성 후, jwt token 값을 입력해줍니다.
그다음은 views.py를 보겠습니다.
@api_view(['POST'])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def like_post(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.user in post.likes.all():
post.likes.remove(request.user)
else:
post.likes.add(request.user)
serializer = BlogPostSerializer(post)
return Response(serializer.data)
데코레이터부터 순서대로 설명드리자면,
@api_view(['POST'])
는 해당 뷰(like_post 메소드)는 POST요청만 처리하겠다라는 뜻입니다.
@authentication_classes([JWTAuthentication])
는 JWTAuthentication 인증방식을 사용한다는 뜻입니다.
@permission_classes([IsAuthenticated])
는 인증된 사용자만 접근 가능하다는 뜻입니다.
(💡Tip. 해당 데코레이터를 사용함으로써 소스내부에 request.user.is_authenticated 같은 인증과정을 거칠 필요가 없어집니다.)
like기능에 대한 자세한 내용은 이전 포스트에 작성해두었으니, 참고 부탁드립니다. (해당 포스트의 기준은 jwt토큰을 사용하기전인 session기반 인증입니다.)
이제 해당 버튼을 클릭할때를 보겠습니다.
스크린샷에서 보는것과 같이 client로부터 요청을 받습니다.
요약탭과 요청탭을 보면, client는 127.0.0.1:8000/blog/5/like 로 요청을 보냈고, 해당 요청은 POST였으며, Authorization에 위에서 언급한 Bearer
이후 서버는 jwt_token이 유효한지 확인 하고, 실제로 like함수를 실행하여 사용자에게 보여줍니다.
다음 포스트에서는 blacklist를 활용하여 토큰의 재사용방지 방법을 설명드리겠습니다.
Leave a Comment: