LostCatBox

DRF 기본편 13

Word count: 1.7kReading time: 10 min
2020/01/26 30 Share

EP 13. Token 인증

DRF에서 지원하는 인증

  • rest_framework.authentication.SessionAuthentication

    • 외부서비스/앱에서세션인증을못쓰죠.
  • rest_framework.authentication.BasicAuthentication

    • 외부서비스/앱에서 매번요청시 username/password를넘기는것은보안상위험하고,못할 짓.
  • rest_framework.authentication.TokenAuthentication (강추)

    • 초기에username/password으로 Token을발급받고,(유저마다 하나의 토큰이며, 언제든 폐기가능)
    • 이Token을 매API요청에 담아서 보내어 인증을처리

시작하기전에-기본코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 앱/models.py

class Post(models.Model):
#이미 다른 앱에서도 usermodel참고하고있으므로 reverse가 지정불가, related_name지정필요한데 '+'값을 주면 사용안됨
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE, related_name='+')
message = models.TextField(blank=True)
photo = models.ImageField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)

# 앱/serializers.py
from rest_framework.fields import ReadOnlyField
from rest_framework.serializers import ModelSerializer

class PostSerializer(ModelSerializer):
author_username = ReadOnlyField(source='author.username')

class Meta:
model = Post
fields = ('id', 'author_username', 'message', 'photo')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 앱/views.py
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet

class PostModelViewSet(ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
authentication_classes = [TokenAuthentication] #세션인증, basic에 유저인증 은 안됨
permission_classes = [IsAuthenticated] #인증이 되어야만 처리

def perform_create(self, serializer):
serializer.save(author=self.request.user)

# 앱/urls.py
router = DefaultRouter()
router.register('post', PostModelViewSet)

urlpatterns = [
url(r'', include(router.urls)),
]

Token인증설정하기

1
2
3
# settings
INSTALLED_APPS = ( 'rest_framework.authtoken',
)
  • migrate가 필요합니다.
  • rest_framework.authtoken.Token모델에 대한 테이블을 생성해야 합니다

Token모델

  • 각 User에 대해 1:1 Relation => OneToOneField
  • 각User별 Token인스턴스가자동생성되지않습니다.(모델생성했다고 자동생성절대안됨)
  • Token은 유저 별로 Unique합니다. Token만으로 인증을 수행합니다.
1
2
3
4
5
# rest_framework/authotken/models.py

class Token(models.Model): # 모델 코드에서 주요부분만 발췌
key = models.CharField(max_length=40, primary_key=True)
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='auth_token')

Token생성

방법1) ObtatinAuthToken뷰를 통한 획득 or 생성

token 획득 API 뷰에서 Token 획득/생성을 아래 코드와 같이 처리하므로, Token 재생성이 아니라면 특별히 처리해줄 필요는 없습니다.

1
2
3
4
5
6
7
# rest_framework/authtoken/views.py

class ObtainAuthToken(APIView):
def post(self, request, *args, **kwargs):
# 중략
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})

방법2) Signal을 통한 자동 생성 (필수는 아님)(이벤트헨들러느낌)

1
2
3
4
5
6
7
8
9
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(post_save, sender=settings.AUTH_USER_MODEL) # post_save 처럼 save가 일어나는건데 그게 AUTH_USER_MODEL일어날 경우 끝나고 아래 함수 실행
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created: #save할때가 created or updated중에 created일때 작동
Token.objects.create(user=instance)

방법3) Management 명령을 통한 생성

1
2
3
4
# 본 명령은 생성된 Token을 변경하지 않습니다. 필수는 아님. 
python3 manage.py drf_create_token <username>
# 강제로 Token 재생성하기
python3 manage.py drf_create_token -r <username>

Token 획득을 API ENDPOINT로 노출하기

post만 처리가능함

1
2
3
4
5
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns += [
url(r'^api-token-auth/', obtain_auth_token),
]

HTTPie를 통한 Token 획득

1
쉘> http POST http://주소/api-token-auth/ username="유저명" password="암호"
1
2
3
4
5
6
7
HTTP/1.0 200 OK
Allow: POST, OPTIONS
Content-Type: application/json
Date: Fri, 01 Dec 2017 06:49:35 GMT Server: WSGIServer/0.2 CPython/3.6.1
{
"token": "9cdd705c0a0e5adb8671d22bd6c7a99bbacab227"
}

HTTPie 예제 (/리눅스)

-헤더에 토큰값 넣어서 요청 보내야함(띄어쓰기 잘하기)

1
http http://localhost:8000/api/post/ "Authorization: Token <TOKEN값입력>"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#환경변수를 설정해서 편하게 이용하자
export HOST="http://localhost:8000"
export TOKEN="d7ec115d8370de00f06c50ec5b61bd519b1038ae"

# Post List
http $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" # httpie 문법임, 현재 프로젝트전체폴에경로에존재하는 사진파일

# 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"

파이썬 코드를 통한 Token 획득

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

HOST = 'http://localhost:8000'

res = requests.post(HOST + '/api-token-auth/', {
'username': '유저명', # FIXME: 기입해주세요.
'password': '암호', # FIXME: 기입해주세요.
})
res.raise_for_status()

token = res.json()['token']

print(token)

이제 모든 요청에는 다음 인증헤더를 붙여주셔야 합니다.

headers = {
'Authorization': 'Token ' + token, # 필히 띄워쓰기 꼭 ㄴ필요
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Post List
res = requests.get(HOST + '/api/post/', headers=headers) res.raise_for_status()
print(res.json())

# Post Create
data = {'message': 'hello requests'}
res = requests.post(HOST + '/api/post/', data=data, headers=headers) print(res)
print(res.json())

# Post Create with Photo
files = {'photo': open('f1.jpg', 'rb')}
data = {'message': 'hello requests'}
res = requests.post(HOST + '/api/post/', files=files, data=data, headers=headers)
print(res)
print(res.json())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Post#16 Detail
res = requests.get(HOST + '/api/post/', headers=headers)
res = requests.get(HOST + '/api/post/16/', headers=headers) res.raise_for_status()
print(res.json())

# Post#16 Patch
res = requests.patch(HOST + '/api/post/16/', headers=headers, data={'message': 'hello'})
res.raise_for_status()
print(res.json())

# Post#16 Update
res = requests.put(HOST + '/api/post/16/', headers=headers, data={'message': 'hello'})
res.raise_for_status()
print(res.json())

# Post#16 Delete
res = requests.delete(HOST + '/api/post/16/', headers=headers, data={'message': 'hello'})
res.raise_for_status()
print(res.ok)

실제 터미널

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
In [1]: import requests

In [3]: res = requests.post('http://localhost:8000/api-auth-token/', {'username'
...: :'tester', 'password': 'rhdiddl486'})

In [4]: res = requests.post('http://localhost:8000/api-token-auth/', {'username'
...: :'tester', 'password': 'rhdiddl486'})

In [5]: res
Out[5]: <Response [200]>

In [6]: res.text
Out[6]: '{"token":"d7ec115d8370de00f06c50ec5b61bd519b1038ae"}'

In [7]: res.json()
Out[7]: {'token': 'd7ec115d8370de00f06c50ec5b61bd519b1038ae'}

In [8]: res.json()['token']
Out[8]: 'd7ec115d8370de00f06c50ec5b61bd519b1038ae'

In [9]: token = res.json()['token']

In [10]: headers = {
...: 'Authorization': 'Token ' + token,
...: }

In [11]: headers
Out[11]: {'Authorization': 'Token d7ec115d8370de00f06c50ec5b61bd519b1038ae'}

In [12]: res = requests.get('http://localhost:800/api/post/', headers=headers)

In [13]: res = requests.get('http://localhost:8000/api/post/', headers=headers)

In [14]: res
Out[14]: <Response [200]>

In [15]: res.text
Out[15]: '[{"id":1,"author_username":"tester","message":"Hello","photo":null},{"id":2,"author_username":"tester","message":"Hello","photo":null},{"id":3,"author_username":"tester","message":"Hello","photo":null},{"id":4,"author_username":"tester","message":"Hello","photo":null},{"id":5,"author_username":"tester","message":"Hello","photo":null},{"id":6,"author_username":"tester","message":"Hello","photo":null},{"id":7,"author_username":"tester","message":"Hello","photo":null},{"id":8,"author_username":"tester","message":"Hello","photo":null},{"id":9,"author_username":"tester","message":"Hello","photo":null},{"id":10,"author_username":"tester","message":"Hello","photo":null},{"id":11,"author_username":"tester","message":"Hello","photo":null},{"id":12,"author_username":"tester","message":"Hello","photo":"http://localhost:8000/api/post/a_7Y7nc8e.jpg"}]'

In [16]: res.json()
Out[16]:
[{'id': 1, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 2, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 3, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 4, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 5, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 6, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 7, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 8, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 9, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 10, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 11, 'author_username': 'tester', 'message': 'Hello', 'photo': None},
{'id': 12,
'author_username': 'tester',
'message': 'Hello',
'photo': 'http://localhost:8000/api/post/a_7Y7nc8e.jpg'}]

In [17]: for post in res.json():
...: print(post['message'])
...:

In [17]: for post in res.json():
...: print(post['message'])
...:
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello

In [19]: for post in res.json():
...: print('{id}: {message}'.format(**post))
...:
...:
...:
1: Hello
2: Hello
3: Hello
4: Hello
5: Hello
6: Hello
7: Hello
8: Hello
9: Hello
10: Hello
11: Hello
12: Hello

python pack unpack 반드시 알아보기 (???)

CATALOG
  1. 1. EP 13. Token 인증
  2. 2. python pack unpack 반드시 알아보기 (???)