RESTful API에 대한 토큰 인증 : 토큰을 주기적으로 변경해야합니까?


115

Django 및 django-rest-framework 로 RESTful API를 구축하고 있습니다.

인증 메커니즘으로 우리는 "토큰 인증"을 선택했고 Django-REST-Framework의 문서에 따라 이미 구현했습니다. 문제는 애플리케이션이 토큰을 주기적으로 갱신 / 변경해야하며 그렇다면 어떻게해야합니까? 토큰을 갱신해야하는 모바일 앱일까요 아니면 웹 앱이 자율적으로해야할까요?

모범 사례는 무엇입니까?

여기에 Django REST Framework에 대한 경험이 있고 기술 솔루션을 제안 할 수있는 사람이 있습니까?

(마지막 질문이 우선 순위가 낮음)

답변:


101

모바일 클라이언트가 주기적으로 인증 토큰을 갱신하도록하는 것이 좋습니다. 이것은 물론 서버에 달려 있습니다.

기본 TokenAuthentication 클래스는이를 지원하지 않지만이를 확장하여이 기능을 수행 할 수 있습니다.

예를 들면 :

from rest_framework.authentication import TokenAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed

class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        # This is required for the time comparison
        utc_now = datetime.utcnow()
        utc_now = utc_now.replace(tzinfo=pytz.utc)

        if token.created < utc_now - timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return token.user, token

또한 로그인이 완료 될 때마다 토큰이 새로 고쳐 지도록 기본 나머지 프레임 워크 로그인보기를 재정의해야합니다.

class ObtainExpiringAuthToken(ObtainAuthToken):
    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            token, created =  Token.objects.get_or_create(user=serializer.validated_data['user'])

            if not created:
                # update the created time of the token to keep it valid
                token.created = datetime.datetime.utcnow()
                token.save()

            return Response({'token': token.key})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

obtain_expiring_auth_token = ObtainExpiringAuthToken.as_view()

그리고 URL을 수정하는 것을 잊지 마십시오.

urlpatterns += patterns(
    '',
    url(r'^users/login/?$', '<path_to_file>.obtain_expiring_auth_token'),
)

6
만료 된 경우 ObtainExpiringAuthToken에서 이전 토큰의 타임 스탬프를 업데이트하는 대신 새 토큰을 만들고 싶지 않습니까?
Joar Leth 2013 년

4
새 토큰을 만드는 것이 합리적입니다. 기존 토큰 키의 값을 다시 생성 할 수도 있으며 그러면 이전 토큰을 삭제할 필요가 없습니다.
odedfos 2013

만료시 토큰을 지우려면 어떻게해야합니까? 다시 get_or_create하면 ​​새 토큰이 생성되거나 타임 스탬프가 업데이트됩니까?
Sayok88

3
또한 유효성 검사를 가로채는 대신 cronjob (Celery Beat 또는 유사)에서 주기적으로 오래된 토큰을 제거하여 테이블에서 토큰을 만료시킬 수 있습니다
BjornW

1
@BjornW 그냥 제거를 수행하고, 제 생각에는 API (또는 귀하의 프런트 엔드)와 통합하여 요청을하는 사람의 책임이며 "유효하지 않은 토큰"을 수신 한 다음 새로 고침 / 새로운 토큰 엔드 포인트 생성
ShibbySham

25

누군가는이 솔루션이 관심을하지만 도착 일정 시간 동안 유효 토큰이 원하는 경우 새로운 토큰으로 대체 여기에 완벽한 솔루션 (장고 1.6)의를 :

yourmodule / views.py :

import datetime
from django.utils.timezone import utc
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from django.http import HttpResponse
import json

class ObtainExpiringAuthToken(ObtainAuthToken):
    def post(self, request):
        serializer = self.serializer_class(data=request.DATA)
        if serializer.is_valid():
            token, created =  Token.objects.get_or_create(user=serializer.object['user'])

            utc_now = datetime.datetime.utcnow()    
            if not created and token.created < utc_now - datetime.timedelta(hours=24):
                token.delete()
                token = Token.objects.create(user=serializer.object['user'])
                token.created = datetime.datetime.utcnow()
                token.save()

            #return Response({'token': token.key})
            response_data = {'token': token.key}
            return HttpResponse(json.dumps(response_data), content_type="application/json")

        return HttpResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

obtain_expiring_auth_token = ObtainExpiringAuthToken.as_view()

yourmodule / urls.py :

from django.conf.urls import patterns, include, url
from weights import views

urlpatterns = patterns('',
    url(r'^token/', 'yourmodule.views.obtain_expiring_auth_token')
)

프로젝트 urls.py (urlpatterns 배열에 있음) :

url(r'^', include('yourmodule.urls')),

yourmodule / authentication.py :

import datetime
from django.utils.timezone import utc
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions

class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):

        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        utc_now = datetime.datetime.utcnow()

        if token.created < utc_now - datetime.timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return (token.user, token)

REST_FRAMEWORK 설정에서 ExpiringTokenAuthentication을 TokenAuthentication 대신 Authentification 클래스로 추가합니다.

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        #'rest_framework.authentication.TokenAuthentication',
        'yourmodule.authentication.ExpiringTokenAuthentication',
    ),
}

'ObtainExpiringAuthToken' object has no attribute 'serializer_class'API 끝점에 액세스하려고 하면 오류가 발생 합니다. 내가 무엇을 놓치고 있는지 잘 모르겠습니다.
Dharmit

2
나중에 테스트 할 흥미로운 솔루션입니다. 현재 귀하의 게시물은 AUTHENTICATION_CLASSES를 설정하는 것을 잊었 기 때문에 올바른 길을가는 데 도움이되었습니다.
normic

2
파티에 늦었지만 제대로 작동하려면 약간의 변경이 필요했습니다. 1) utc_now = datetime.datetime.utcnow ()는 utc_now = datetime.datetime.utcnow (). replace (tzinfo = pytz.UTC) 2) 클래스 ExpiringTokenAuthentication (TokenAuthentication) : 모델이 필요합니다. self.model = self. get_model ()
Ishan 밧

5

@odedfos 대답을 시도했지만 잘못된 오류가 있습니다. 여기에 동일한 대답이 있습니다.

views.py

from django.utils import timezone
from rest_framework import status
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken

class ObtainExpiringAuthToken(ObtainAuthToken):
    def post(self, request):
        serializer = self.serializer_class(data=request.DATA)
        if serializer.is_valid():
            token, created =  Token.objects.get_or_create(user=serializer.object['user'])

            if not created:
                # update the created time of the token to keep it valid
                token.created = datetime.datetime.utcnow().replace(tzinfo=utc)
                token.save()

            return Response({'token': token.key})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

authentication.py

from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions

EXPIRE_HOURS = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_HOURS', 24)

class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        if token.created < timezone.now() - timedelta(hours=EXPIRE_HOURS):
            raise exceptions.AuthenticationFailed('Token has expired')

        return (token.user, token)

4

DRY를 사용하여 Django 2.0 답변을 줄 것이라고 생각했습니다. 누군가 이미 우리를 위해 Google Django OAuth ToolKit을 구축했습니다. 핍 사용 가능, pip install django-oauth-toolkit. 라우터가있는 토큰 ViewSet 추가에 대한 지침 : https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . 공식 튜토리얼과 비슷합니다.

그래서 기본적으로 OAuth1.0은 TokenAuthentication이 무엇인지 더 어제의 보안이었습니다. 만료되는 멋진 토큰을 얻으려면 OAuth2.0이 요즘 대세입니다. AccessToken, RefreshToken 및 범위 변수를 가져와 권한을 미세 조정합니다. 다음과 같은 신용으로 끝납니다.

{
    "access_token": "<your_access_token>",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "<your_refresh_token>",
    "scope": "read"
}

4

저자는 물었다

문제는 애플리케이션이 토큰을 주기적으로 갱신 / 변경해야하며 그렇다면 어떻게해야합니까? 토큰을 갱신해야하는 모바일 앱일까요 아니면 웹 앱이 자율적으로해야할까요?

그러나 모든 답변은 토큰을 자동으로 변경하는 방법에 대해 작성하고 있습니다.

주기적으로 토큰을 변경하는 것은 의미가 없다고 생각합니다. 나머지 프레임 워크는 40 자의 토큰을 생성합니다. 공격자가 매초 1000 개의 토큰을 테스트 16**40/1000/3600/24/365=4.6*10^7하면 토큰을 얻는 데 몇 년이 걸립니다. 공격자가 토큰을 하나씩 테스트 할 것이라고 걱정해서는 안됩니다. 토큰을 변경하더라도 토큰을 추측 할 확률은 같습니다.

공격자가 토큰을 얻을 수 있을지 걱정되면 공격자가 토큰을 얻은 후보다 주기적으로 토큰을 변경하면 실제 사용자가 쫓겨나는 것보다 토큰을 변경할 수도 있습니다.

실제로해야 할 일은 공격자가 사용자의 토큰을 얻지 못하도록 방지 하는 것 입니다. https를 사용하십시오 .

그건 그렇고, 토큰별로 토큰을 변경하는 것은 의미가 없으며 사용자 이름과 암호로 토큰을 변경하는 것은 때때로 의미가 있습니다. 토큰이 일부 http 환경 (항상 이런 상황을 피해야 함) 또는 타사 (이 경우에는 다른 종류의 토큰을 생성하고 oauth2를 사용해야 함)에서 사용되며 사용자가 변경과 같은 위험한 작업을 수행 할 때 메일 함을 바인딩하거나 계정을 삭제하는 경우, 공격자가 스니퍼 또는 tcpdump 도구를 사용하여 공개했을 수 있으므로 더 이상 원본 토큰을 사용하지 않도록해야합니다.


예, 동의합니다. 이전 액세스 토큰이 아닌 다른 방법으로 새 ​​액세스 토큰을 받아야합니다. 새로 고침 토큰 (또는 최소한 암호로 새 로그인을 강제하는 이전 방법)과 같습니다.
BjornW


1

토큰이 세션 쿠키와 비슷하다는 것을 알게된다면 Django에서 세션 쿠키의 기본 수명을 유지할 수 있습니다 : https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age .

Django Rest Framework가 자동으로 처리하는지는 모르겠지만 오래된 스크립트를 필터링하고 만료 된 것으로 표시하는 짧은 스크립트를 언제든지 작성할 수 있습니다.


1
토큰 인증은 쿠키를 사용하지 않습니다
s29

0

이것이 나를 위해 도움이 되었기 때문에 내가 추가 할 것이라고 생각했습니다. 나는 일반적으로 JWT 방법을 사용하지만 때로는 이와 같은 것이 더 좋습니다. 적절한 가져 오기로 django 2.1에 대한 허용 답변을 업데이트했습니다.

authentication.py

from datetime import timedelta
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions

EXPIRE_HOURS = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_HOURS', 24)


class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        try:
            token = self.get_model().objects.get(key=key)
        except ObjectDoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        if token.created < timezone.now() - timedelta(hours=EXPIRE_HOURS):
            raise exceptions.AuthenticationFailed('Token has expired')

    return token.user, token

views.py

import datetime
from pytz import utc
from rest_framework import status
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.serializers import AuthTokenSerializer


class ObtainExpiringAuthToken(ObtainAuthToken):
    def post(self, request, **kwargs):
        serializer = AuthTokenSerializer(data=request.data)

        if serializer.is_valid():
            token, created = Token.objects.get_or_create(user=serializer.validated_data['user'])
            if not created:
                # update the created time of the token to keep it valid
                token.created = datetime.datetime.utcnow().replace(tzinfo=utc)
                token.save()

            return Response({'token': token.key})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

0

@odedfos 답변에 계속 추가하기 위해 구문이 변경되어 ExpiringTokenAuthentication 코드에 약간의 조정이 필요하다고 생각합니다.

from rest_framework.authentication import TokenAuthentication
from datetime import timedelta
from datetime import datetime
import datetime as dtime
import pytz

class ExpiringTokenAuthentication(TokenAuthentication):

    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted')

        # This is required for the time comparison
        utc_now = datetime.now(dtime.timezone.utc)
        utc_now = utc_now.replace(tzinfo=pytz.utc)

        if token.created < utc_now - timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return token.user, token

또한 rest_framework.authentication.TokenAuthentication 대신 DEFAULT_AUTHENTICATION_CLASSES에 추가하는 것을 잊지 마십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.