RESTful API için Token Kimlik Doğrulaması: jeton periyodik olarak değiştirilmeli mi?


115

Django ve django-rest-framework ile bir RESTful API oluşturuyorum .

Kimlik doğrulama mekanizması olarak "Token Authentication" ı seçtik ve bunu Django-REST-Framework belgelerine göre zaten uyguladım, soru şu ki, uygulama Token'ı periyodik olarak yenilemeli / değiştirmeli mi ve evet ise nasıl? Jetonun yenilenmesini gerektiren mobil uygulama mı yoksa web uygulaması bunu bağımsız olarak mı yapmalıdır?

En iyi uygulama nedir?

Burada Django REST Framework konusunda deneyimli olan ve teknik bir çözüm önerebilecek olan var mı?

(son sorunun önceliği daha düşüktür)

Yanıtlar:


102

Mobil istemcilerin kimlik doğrulama jetonlarını düzenli aralıklarla yenilemeleri iyi bir uygulamadır. Bu tabii ki zorunlu kılmak için sunucuya bağlıdır.

Varsayılan TokenAuthentication sınıfı bunu desteklemez, ancak bu işlevi elde etmek için genişletebilirsiniz.

Örneğin:

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

Ayrıca, bir oturum açıldığında jetonun yenilenmesi için varsayılan dinlenme çerçevesi oturum açma görünümünü geçersiz kılmak gerekir:

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()

Ve url'leri değiştirmeyi unutmayın:

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

6
ObtainExpiringAuthToken'da, süresi dolmuşsa, eskisinin zaman damgasını güncellemek yerine yeni bir belirteç oluşturmak istemez miydiniz?
Joar Leth

4
Yeni bir belirteç oluşturmak mantıklı. Mevcut belirteç anahtarının değerini de yeniden oluşturabilir ve ardından eski belirteci silmeniz gerekmez.
odedfos

Jetonu süresi dolduğunda temizlemek istersem ne olur? Tekrar get_or_create yaptığımda yeni bir jeton oluşturulacak mı yoksa zaman damgası güncellenecek mi?
Sayok88

3
Ayrıca, doğrulamayı durdurmak yerine, bir cronjob (Celery Beat veya benzeri) ile eski olanları periyodik olarak çıkararak tablodaki jetonların kullanım süresini sona erdirebilirsiniz
BjornW

1
@BjornW Ben sadece tahliyeyi yapardım ve bence, API (veya ön ucunuzla) entegre olan kişinin bir talepte bulunma sorumluluğu, "Geçersiz simge" alır ve ardından yenileme / yeni belirteç uç noktaları oluşturma
ShibbySham

25

Birisi bu çözümle ilgileniyor ancak belirli bir süre için geçerli olan bir jetona sahip olmak istiyorsa, yeni bir jetonla değiştirilirse, işte tam çözüm (Django 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')
)

projeniz url.py (urlpatterns dizisinde):

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 ayarlarınızda, TokenAuthentication yerine ExpiringTokenAuthentication'ı Authentification sınıfı olarak ekleyin:

REST_FRAMEWORK = {

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

'ObtainExpiringAuthToken' object has no attribute 'serializer_class'API uç noktasına erişmeye çalıştığımda hata alıyorum . Ne kaçırdığımdan emin değilim.
Dharmit

2
Daha sonra deneyeceğim ilginç bir çözüm; AUTHENTICATION_CLASSES ayarlamayı unuttuğum için gönderiniz doğru yola girmeme yardımcı oldu.
normic

2
Partiye geç geliyorum ama işe yaraması için bazı ince değişiklikler yapmam gerekiyordu. 1) utc_now = datetime.datetime.utcnow () utc_now = datetime.datetime.utcnow () olmalıdır. Replace (tzinfo = pytz.UTC) 2) Sınıfta ExpiringTokenAuthentication (TokenAuthentication): Modele ihtiyacınız var, self.model = self. get_model ()
Ishan Bhatt

5

@Odedfos yanıtını denedim ama yanıltıcı bir hata aldım . İşte aynı cevap, sabit ve uygun ithalatla.

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 kullanarak Django 2.0 cevabı vereceğimi düşündüm. Bunu bizim için zaten birisi oluşturdu, google Django OAuth ToolKit. Pip ile mevcuttur pip install django-oauth-toolkit. Yönlendiricilerle belirteç ViewSets ekleme talimatları: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . Resmi öğreticiye benzer.

Yani temelde OAuth1.0, daha dünün güvenliğiydi ve TokenAuthentication da buydu. Süresi dolan süslü jetonlar almak için, OAuth2.0 bugünlerde çok popüler. İzinlere ince ayar yapmak için bir AccessToken, RefreshToken ve kapsam değişkeni alırsınız. Bunun gibi kredilere sahip olursunuz:

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

4

Yazar sordu

soru şudur, uygulama Token'ı periyodik olarak yenilemeli / değiştirmeli mi ve evet ise nasıl? Jetonun yenilenmesini gerektiren mobil uygulama mı yoksa web uygulaması bunu bağımsız olarak mı yapmalıdır?

Ancak tüm cevaplar, jetonun otomatik olarak nasıl değiştirileceği hakkında yazıyor.

Jetonla periyodik olarak jeton değiştirmenin anlamsız olduğunu düşünüyorum. Geri kalan çerçeve, 40 karakter içeren bir belirteç oluşturur, saldırgan her saniyede 1000 jetonu test ederse jetonu 16**40/1000/3600/24/365=4.6*10^7almak için yıllar gerekir. Saldırganın jetonunuzu tek tek test edeceğinden endişelenmemelisiniz. Jetonunuzu değiştirmiş olsanız bile, jetonunuzu tahmin etme olasılığı aynıdır.

Saldırganların size jeton alabileceğinden endişeleniyorsanız, bu yüzden onu periyodik olarak değiştirirseniz, saldırgan jetonu aldıktan sonra, jetonunuzu gerçek kullanıcının atıldığından daha değiştirebilir.

Gerçekte yapmanız gereken şey, bir saldırganın kullanıcınızın jetonunu almasını önlemek için https kullanın .

Bu arada, sadece belirteçten jeton değiştir anlamsız, kullanıcı adı ve şifre ile belirteç değiştirmenin bazen anlamsız olduğunu söylüyorum. Belki jeton bazı http ortamında (bu tür durumlardan her zaman kaçınmalısınız) veya bazı üçüncü taraflarda (bu durumda farklı türde bir simge oluşturmalısınız, oauth2 kullanın) ve kullanıcı değiştirmek gibi tehlikeli bir şey yaptığında kullanılır posta kutusunu bağlama veya hesabı silme, kaynak belirtecini artık kullanmadığınızdan emin olmalısınız çünkü bu, saldırgan tarafından sniffer veya tcpdump araçları kullanılarak ortaya çıkarılmış olabilir.


Evet, kabul ediyorum, başka yollarla (eski bir erişim belirtecinden) yeni bir erişim belirteci almalısınız. Yenileme belirtecinde olduğu gibi (veya en azından parola ile yeni bir oturum açmaya zorlamanın eski yolu).
BjornW


1

Bir belirtecin bir oturum tanımlama bilgisi gibi olduğunu fark ederseniz, Django'da oturum tanımlama bilgilerinin varsayılan yaşam süresine bağlı kalabilirsiniz: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age .

Django Rest Framework bunu otomatik olarak hallediyor mu bilmiyorum ama her zaman güncel olmayanları filtreleyen ve onları süresi dolmuş olarak işaretleyen kısa bir komut dosyası yazabilirsiniz.


1
Token Kimlik Doğrulaması çerez kullanmıyor
s29

0

Bu benim için yararlı olduğu için benimkini de ekleyeceğimi düşündüm. Genellikle JWT yöntemini kullanırım ama bazen böyle bir şey daha iyidir. Django 2.1 için kabul edilen yanıtı uygun ithalatlarla güncelledim.

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 yanıtına eklemeye devam etmek için, söz diziminde bazı değişiklikler olduğunu düşünüyorum, bu nedenle ExpiringTokenAuthentication kodunun biraz ayarlanması gerekiyor:

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

Ayrıca, rest_framework.authentication.TokenAuthentication yerine DEFAULT_AUTHENTICATION_CLASSES'e eklemeyi unutmayın.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.