이전에 살펴 본 DRF에서 지원하는 인증 중에 TokenAuthentication 이 있습니다. SessionAuthentication 과 BasicAuthentication 은 아무래도 아래와 같은 문제점이 있다보니 TokenAuthentication 을 쓰는 편입니다.
rest_framework.authentication.SessionAuthentication
rest_framework.authentication.BasicAuthentication
username
과 password
를 넘기는 것은 보안상 위험함.rest_framework.authentication.TokenAuthentication
username
과 password
으로 Token 을 발급받고, 이 Token 을 매 API요청에 담아 보내 인증을 처리.🦝 django-rest-framework/rest_framework/authtoken/models.py
# rest_framework/authtoken/models.py
class Token(models.Model):
"""
The default authorization token model.
"""
key = models.CharField(_("Key"), max_length=40, primary_key=True)
user = models.OneToOneField(
settings.AUTH_USER_MODEL, related_name='auth_token',
on_delete=models.CASCADE, verbose_name=_("User")
)
created = models.DateTimeField(_("Created"), auto_now_add=True)
class Meta:
# Work around for a bug in Django:
# https://code.djangoproject.com/ticket/19422
#
# Also see corresponding ticket:
# https://github.com/encode/django-rest-framework/issues/705
abstract = 'rest_framework.authtoken' not in settings.INSTALLED_APPS
verbose_name = _("Token")
verbose_name_plural = _("Tokens")
def save(self, *args, **kwargs):
if not self.key: # self.key에 값이 없으면 랜덤 문자열 지정
self.key = self.generate_key()
return super().save(*args, **kwargs)
@classmethod
def generate_key(cls):
return binascii.hexlify(os.urandom(20)).decode()
def __str__(self):
return self.key
Token을 생성하는 방법은 3가지가 있습니다. 그 중에 ObtainAuthToken 방법을 이용해 생성해 볼겁니다.
먼저, DRF에서 지원해주는 authtoken
앱을 설정해줍니다. 앱의 일종이며 Token 이라는 model을 지원하므로 migrate 과정도 필요합니다. Django 에서는 default Authentication 으로 SessionAuthenticaton 과 BasicAuthentication 만을 사용하고 있으므로, TokenAuthentication 도 추가해줍니다.
# settings.py
INSTALLED_APPS = [
#...
'rest_framework.authtoken',
]
# ...
RESTFRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthenticaiotn',
'rest_framework.authentication.BasicAuthenticaiotn',
'rest_framework.authentication.TokenAuthentication',
],
}
$ python manage.py migrate
이 방법은 URL Pattern 매핑이 필요합니다.
# rest_framework/authtoken/views.py
class ObtainAuthToken(APIView):
def post(self, request, *args, **kwargs): # post 요청이 들어오면
#...
token, created = Token.objects.get_or_create(user=user) # 해당 user의 token을 가져오거나, 아직 token이 없으면 token을 생성한다.
return Response
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
# receiver() : sender 변수에 User 모델을 지정 해서 해당 모델이 save 될 때 호출한다.
@receiver(post_save, senders=settings.AUTH_USER_MODEL):
def create_auth_token(sener, instance=None, created=False, **kwargs):
if created: # create/update 모두 save 이므로, created 일때만 token을 생성하게 한다.
Token.objects.create(user=instance)
# 본 명령은 생성된 Token을 변경하지 않습니다. 필수 X
$ python3 manage.py drf_create_token <username>
# 강제로 Token 재생성하기
$ python3 manage.py drf_create_token -r <username>
token 흭득을 API endpoint 로 노출시키기 위해 URL Pattern 매핑을 진행합니다.
# accounts/urls.py
from rest_framework.authtoekn.views import obtain_auth_token
urlpatterns = [
path('api-token-auth/', obtain_auth_token),
]
shell 에서 올바른 password와 함께 POST 요청을 보내면 token 이 생성된 것을 확인할 수 있습니다. 이 때 계속되는 요청에 대해선 토큰을 계속 생성하는 것이 아닌, 이미 만들어뒀던 토큰을 반환합니다.
> http POST localhost:8000/accounts/api-token-auth/ username=<username> password=<password>
HTTP/1.1 200 OK
Allow: POST, OPTIONS
Content-Length: 52
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sat, 05 Feb 2022 11:53:31 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.2
Vary: Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"token": "ff199ba3d83de440a31e55c48494f906c8cfae2e"
}
shell 에서 다음과 같은 명령어를 사용하면 token 값을 저장시킬 수 있습니다.
> export HOST="http://localhost:8000"
> export TOKEN="ff199ba3d83de440a31e55c48494f906c8cfae2e"
이제 더 간단히 요청을 보낼 수 있습니다.
# Post List
http GET $HOST/api/post/ "Authorization: Token $TOKEN"
# Post Create
http POST $HOST/api/post/ "Authorization: Token $TOKEN" message="hello"
# Post Create with Photo
http --form POST $HOST/api/post/ "Authorization: Token $TOKEN" message="hello" photo@"f1.jpg"
# Post#16 Detail
http GET $HOST/api/post/16/ "Authorization: Token $TOKEN"
# Post#16 Update
http PATCH $HOST/api/post/16/ "Authorization: Token $TOKEN" message="patched"
http PUT $HOST/api/post/16/ "Authorization: Token $TOKEN" message="updated"
# Post#16 Delete
http DELETE $HOST/api/post/16/ "Authorization: Token $TOKEN"
이제 post 요청시에 token을 넘기지 않으면 인증이 필요하다는 메시지가 나타납니다.
> http localhost:8000/post/
HTTP/1.1 403 Forbidden
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Length: 58
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sat, 05 Feb 2022 12:19:58 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.2
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"detail": "Authentication credentials were not provided."
}
token을 넘겨주면 정상적으로 요청이 넘어옵니다.
> http localhost:8000/post/ "Authorization: Token $Token"
HTTP/1.1 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Length: 175
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Sat, 05 Feb 2022 12:14:55 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.2
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
{
"author_username": "django",
"created_at": "2022-01-26T02:08:59.293032Z",
"ip": null,
"is_public": false,
"message": "first posting",
"pk": 1,
"updated_at": "2022-01-26T02:08:59.293032Z"
}