본문 바로가기
python

django REST API 서버에 토큰 인증 적용

by misankim 2023. 3. 19.

지난번 django REST API 서버를 구성하여 백업 결과 API를 저장하는 내용으로 학습 내용을 공유해드렸는데요. 이번에는 지난번 구성한 백업 결과 API 서버에 JWT(json web token)를 통한 토큰 인증을 적용하는 방법을 공유하려고 합니다. 먼저 백업 결과 API 서버가 먼저 구성되어 있어야하기 때문에 백업 결과 API 서버 구성 방법은 지난 게시글을 참고 부탁드리겠습니다.

1. JWT 인증이란

https://jwt.io/introduction

JWT(json web token)이란 서버가 최초 클라이언트로부터 전달받은 사용자 정보를 통해 클라이언트로 인증 토큰을 전송하고, 이후 통신에서 클라이언트는 요청 헤더에 JWT 토큰 값을 포함하는 "Authorization" 헤더를 추가하여 서버에 인증을 수행하는 방식입니다.

클라이언트 -> 사용자 정보 전송 -> 서버 -> 토큰 발행 -> 클라이언트 -> 토큰 값을 헤더에 포함하여 전송 -> 서버(인증)

마치 서버가 클라이언트로 세션 값을 쿠키로 전달하여 이후 통신에 서버로부터 전달받은 쿠키 값을 요청에 포함하는 세션 인증과 유사해보이나, 브라우저 상에 쿠키 값을 저장하는 세션 인증과 달리 JWT 토큰 인증은 쿠키 값을 저장하지 않는 모바일 앱, CURL과 같은 API 통신에 유리합니다. 또한 도메인 기반으로 저장되는 쿠키와 달리 JWT 토큰 인증은 토큰 값으로만 인증을 수행하기 때문에 도메인이 다름으로써 발생하는 CORS(Cross-Origin Resource Sharing) 문제를 해결할 수 있습니다.

2. 토큰 인증 적용 전

지난 게시글로 구성한 백업 결과 REST API 서버에 인증 토큰을 적용하기 전의 모습입니다. CURL 호출 시 어떤 인증도 하지 않기 때문에 8000포트만 허용되어 있다면 누구나 API 통신을 할 수 있습니다.

# 백업 결과 리스트 확인
curl -H "Content-Type: application/json" -X GET http://www.premisan01.shop:8000/api/v1/backups/ | jq

# 백업 시작 및 백업 종료
result_NUM=`curl -H "Content-Type: application/json" -X POST -d "{\"Date\": \"$(date +%Y%m%d)\",\"Hostname\": \"$HOSTNAME\", \"PRI_IP\": \"$(ifconfig eth0 | head -2 | tail -1 | sed 's/ //g' | awk -Finet '{ print $2 }' | awk -Fnetmask '{ print $1 }')\", \"PUB_IP\": \"$(curl -s ifconfig.me)\", \"Result\":\"running\"}" http://www.premisan01.shop:8000/api/v1/backups/ | jq -r .NUM`

sleep 5

curl -H "Content-Type: application/json" -X PUT -d "{\"Date\": \"$(date +%Y%m%d)\",\"Hostname\": \"$HOSTNAME\", \"PRI_IP\": \"$(ifconfig eth0 | head -2 | tail -1 | sed 's/ //g' | awk -Finet '{ print $2 }' | awk -Fnetmask '{ print $1 }')\", \"PUB_IP\": \"$(curl -s ifconfig.me)\", \"Result\": \"success\", \"File_check\": \"300/300,100%\"}" http://www.premisan01.shop:8000/api/v1/backups/${result_NUM}/ | jq

3. JWT 토큰 인증 적용하기

django 에서는 django-rest-auth, rest-framework-jwt-auth 와 같이 인증 관련 모듈을 추가하여 쉽게 토큰을 통한 사용자 인증을 설정할 수 있습니다.

django-rest-auth - REST 프레임워크의 인증 기능을 위한 모듈, django-rest-auth 모듈은 기본적으로 django의 토큰 기반 인증을 사용
djangorestframework-jwt - django-rest-auth 모듈에서 jwt 인증을 사용하려면 djangorestframework-jwt 모듈 추가 설치
# 모듈 설치
yum install -y python3 python3-devel gcc rust cargo openssl-devel

python -m pip install djangorestframework
python -m pip install djangorestframework-jwt
python -m pip install django-rest-auth
python -m pip install setuptools-rust
python -m pip install django-allauth

이제 추가한 모듈을 설정 파일을 통해 적용합니다.(백업 API 프로젝트(testapp)의 경로는 /root/python/testapp 으로 설정되어 있는 상태를 기준으로 하였습니다.)

vim /root/python/testapp/testapp/settings.py

INSTALLED_APPS 부분에 아래 내용 추가

INSTALLED_APPS = [
...
    'rest_framework',
    'rest_framework_swagger',
    'backup',
...
    'rest_framework.authtoken',
    'rest_auth',
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'rest_auth.registration',
]

파일 최하단에 아래 내용 추가(DEFAULT_PERMISSION_CLASSES -> 기본 권한 전역 설정, 상세 내용은 https://www.django-rest-framework.org/api-guide/permissions/ 참고)

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',
    ),
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
}

SITE_ID = 1
REST_USE_JWT = True
ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = None
ACCOUNT_LOGOUT_ON_GET = True

저장 후 닫기
vim /root/python/testapp/testapp/urls.py

urlpatterns에 "rest-auth/", "rest-auth/registration/" URL에 대한 설정만 두 줄 추가(가장 하단 두 줄)

from django.conf.urls import url, include
from django.contrib import admin
from rest_framework import routers
from rest_framework_swagger.views import get_swagger_view

import member.api

app_name='member'

router = routers.DefaultRouter()
router.register('members', member.api.MemberViewSet)

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^api/doc', get_swagger_view(title='Rest API Document')),
    url(r'^api/v1/', include((router.urls, 'member'), namespace='api')),
    url(r'^rest-auth/', include('rest_auth.urls')),
    url(r'^rest-auth/registration/', include('rest_auth.registration.urls')),
]

4. 토큰을 발급받을 사용자 생성

사용자 생성은 "http://백업API서버도메인:8000/rest-auth/registration/" URL을 통해 가능하며, 아래 가이드와 같이 json body를 작성하여 POST 방식으로 요청합니다.

테스트에 사용할 "premisantest"라는 이름의 유저를 생성하였습니다. 사용자 생성 시 토큰이 생성되나, 토큰은 매번 새로 생성이 가능하기 때문에 따로 기록해둘 필요는 없습니다.

5. 토큰 인증 적용 후

인증을 적용하고 인증 전과 동일한 커맨드로 백업 리스트를 쿼리하였습니다. 아래와 같이 인증 정보가 제공되지 않았다는 "401 Unauthorized" 응답을 수신합니다.

이제 API 통신을 위해서는 토큰 인증이 필요하기 때문에 API 통신을 위한 CURL 커맨드가 포함된 쉘 스크립트를 아래와 같이 기존 커맨드에서 수정해줍니다.

# 인증 적용 전 리스트 확인 커맨드(쉘)
[root@premisan-i shell]# cat select.sh 
curl -H "Content-Type: application/json" -X GET http://www.premisan01.shop:8000/api/v1/backups/ | jq
# 인증 적용 후 리스트 확인 커맨드(쉘)
[root@premisan-i shell]# cat select.sh
JWT=`curl -s -H "Content-Type: application/json" -X POST -d "{\"username\": \"premisantest\",\"password\": \"my-password\"}" http://www.premisan01.shop:8000/rest-auth/login/ | jq -r .token`

echo JWT=$JWT

curl -s -H "Authorization: JWT $JWT" -X GET http://www.premisan01.shop:8000/api/v1/backups/ | jq

위의 쉘과 같이 API 호출 전 토큰을 발급받고, 백업 결과 리스트 확인 API 호출을 위한 CURL 커맨드에 "Authorization" 헤더를 추가하여 JWT 토큰을 포함한 요청을 서버로 전송합니다. 토큰을 통해 인증을 진행하여 아래와 같이 정상적으로 응답을 받은 것을 확인할 수 있습니다.

백업 시작/종료 시 결과를 전송하는 API의 경우에도 동일하게 토큰 인증이 적용됩니다. 토큰 인증 설정 후 기존 커맨드로 API 호출 시 아래와 같이 인증되지 않았다는 메시지와 함께 401 에러가 수신됩니다.

마찬가지로 쉘스크립트를 수정해줍니다.

# 인증 적용 전 백업 시작/종료 쉘
[root@premisan-i shell]# cat backup.sh 
result_NUM=`curl -H "Content-Type: application/json" -X POST -d "{\"Date\": \"$(date +%Y%m%d)\",\"Hostname\": \"$HOSTNAME\", \"PRI_IP\": \"$(ifconfig eth0 | head -2 | tail -1 | sed 's/ //g' | awk -Finet '{ print $2 }' | awk -Fnetmask '{ print $1 }')\", \"PUB_IP\": \"$(curl -s ifconfig.me)\", \"Result\":\"running\"}" http://www.premisan01.shop:8000/api/v1/backups/ | jq -r .NUM`

sleep 5

curl -H "Content-Type: application/json" -X PUT -d "{\"Date\": \"$(date +%Y%m%d)\",\"Hostname\": \"$HOSTNAME\", \"PRI_IP\": \"$(ifconfig eth0 | head -2 | tail -1 | sed 's/ //g' | awk -Finet '{ print $2 }' | awk -Fnetmask '{ print $1 }')\", \"PUB_IP\": \"$(curl -s ifconfig.me)\", \"Result\": \"success\", \"File_check\": \"300/300,100%\"}" http://www.premisan01.shop:8000/api/v1/backups/${result_NUM}/ | jq
# 인증 적용 후 백업 시작/종료 쉘
[root@premisan-i shell]# cat backup.sh 
JWT=`curl -s -H "Content-Type: application/json" -X POST -d "{\"username\": \"premisantest\",\"password\": \"my-password\"}" http://www.premisan01.shop:8000/rest-auth/login/ | jq -r .token`

echo JWT=$JWT

result_NUM=`curl -H "Content-Type: application/json" -H "Authorization: JWT $JWT" -X POST -d "{\"Date\": \"$(date +%Y%m%d)\",\"Hostname\": \"$HOSTNAME\", \"PRI_IP\": \"$(ifconfig eth0 | head -2 | tail -1 | sed 's/ //g' | awk -Finet '{ print $2 }' | awk -Fnetmask '{ print $1 }')\", \"PUB_IP\": \"$(curl -s ifconfig.me)\", \"Result\":\"running\"}" http://www.premisan01.shop:8000/api/v1/backups/ | jq -r .NUM`

sleep 5

curl -H "Content-Type: application/json" -H "Authorization: JWT $JWT" -X PUT -d "{\"Date\": \"$(date +%Y%m%d)\",\"Hostname\": \"$HOSTNAME\", \"PRI_IP\": \"$(ifconfig eth0 | head -2 | tail -1 | sed 's/ //g' | awk -Finet '{ print $2 }' | awk -Fnetmask '{ print $1 }')\", \"PUB_IP\": \"$(curl -s ifconfig.me)\", \"Result\": \"success\", \"File_check\": \"300/300,100%\"}" http://www.premisan01.shop:8000/api/v1/backups/${result_NUM}/ | jq

토큰 인증 헤더를 포함하지 않았을 때와 달리 아래와 같이 정상적으로 백업 결과 전송이 가능한 것을 확인할 수 있습니다.

6. 기타 참고 사항

사용자 생성 및 로그인 외에도 아래와 같이 사용자 관련 api 사용이 가능합니다.

생성한 사용자 정보는 django 앱에 연동한 DB에 아래와 같이 저장됩니다. 아래는 MySQL을 연동하여 설정된 앱에서 사용자를 생성했을 때의 결과입니다. (별도 DB 엔진을 setting.py 파일에 지정하지 않으면 django는 sqlite를 기본 DB 엔진으로 사용합니다.)