การพิสูจน์ตัวตนโทเค็นสำหรับ RESTful API: ควรเปลี่ยนโทเค็นเป็นระยะหรือไม่


115

ฉันสร้างสงบ API กับ Django และDjango ส่วนที่เหลือกรอบ

ในฐานะกลไกการพิสูจน์ตัวตนเราได้เลือก "Token Authentication" และฉันได้นำไปใช้แล้วตามเอกสารของ Django-REST-Framework คำถามคือแอปพลิเคชันควรต่ออายุ / เปลี่ยน Token เป็นระยะ ๆ หรือไม่และถ้าใช่ต้องทำอย่างไร มันควรจะเป็นแอพมือถือที่ต้องต่ออายุโทเค็นหรือเว็บแอพควรทำแบบอัตโนมัติ?

การปฏิบัติที่ดีที่สุดคืออะไร?

ใครมีประสบการณ์กับ Django REST Framework และสามารถแนะนำวิธีแก้ปัญหาทางเทคนิคได้บ้าง?

(คำถามสุดท้ายมีลำดับความสำคัญต่ำกว่า)

คำตอบ:


102

เป็นแนวทางปฏิบัติที่ดีที่จะให้ไคลเอนต์มือถือต่ออายุโทเค็นการตรวจสอบสิทธิ์เป็นระยะ แน่นอนว่านี่ขึ้นอยู่กับเซิร์ฟเวอร์ที่จะบังคับใช้

คลาส 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

4
การสร้างโทเค็นใหม่นั้นสมเหตุสมผล คุณยังสามารถสร้างค่าของคีย์โทเค็นที่มีอยู่ใหม่ได้จากนั้นคุณจะไม่ต้องลบโทเค็นเก่า
odedfos

จะทำอย่างไรหากต้องการล้างโทเค็นเมื่อหมดอายุ เมื่อฉัน get_or_create อีกครั้งจะมีการสร้างโทเค็นใหม่หรือการประทับเวลาได้รับการอัปเดต?
Sayok88

3
นอกจากนี้คุณสามารถนำโทเค็นออกจากโต๊ะหมดอายุได้โดยการขับไล่โทเค็นเก่าเป็นระยะ ๆ ใน cronjob (Celery Beat หรือที่คล้ายกัน) แทนที่จะสกัดกั้นการตรวจสอบความถูกต้อง
BjornW

1
@BjornW ฉันจะทำการขับไล่และในความคิดของฉันมันเป็นความรับผิดชอบของบุคคลที่ผสานรวมกับ API (หรือส่วนหน้าของคุณ) เพื่อส่งคำขอพวกเขาได้รับ "โทเค็นไม่ถูกต้อง" จากนั้นกดรีเฟรช / สร้างจุดสิ้นสุดโทเค็นใหม่
ShibbySham

25

หากมีคนสนใจโซลูชันนั้น แต่ต้องการมีโทเค็นที่ใช้ได้ในช่วงเวลาหนึ่งก็จะถูกแทนที่ด้วยโทเค็นใหม่นี่คือโซลูชันที่สมบูรณ์ (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')
)

url.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 เป็นคลาส Authentification แทน TokenAuthentication:

REST_FRAMEWORK = {

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

ฉันได้รับข้อผิดพลาด'ObtainExpiringAuthToken' object has no attribute 'serializer_class'เมื่อพยายามเข้าถึงปลายทาง api ไม่แน่ใจว่าฉันขาดอะไรไป
ธรรม

2
วิธีแก้ปัญหาที่น่าสนใจซึ่งฉันจะทดสอบในภายหลัง ในขณะที่โพสต์ของคุณช่วยให้ฉันไปถูกทางเพราะฉันลืมตั้งค่า AUTHENTICATION_CLASSES
ปกติ

2
มางานปาร์ตี้ช้า แต่ฉันต้องทำการเปลี่ยนแปลงเล็กน้อยเพื่อให้มันได้ผล 1) utc_now = datetime.datetime.utcnow () ควรเป็น utc_now = datetime.datetime.utcnow () แทนที่ (tzinfo = pytz.UTC) 2) ในคลาส ExpiringTokenAuthentication (TokenAuthentication): คุณต้องการโมเดล self.model = self get_model ()
Ishan Bhatt

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

คิดว่าฉันจะตอบ Django 2.0 โดยใช้ DRY มีคนสร้างสิ่งนี้ให้เราแล้ว Google Django OAuth ToolKit สามารถใช้ได้กับ pip install django-oauth-toolkitpip, คำแนะนำเกี่ยวกับการเพิ่ม ViewSets โทเค็นกับเราเตอร์: 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

ผู้เขียนถามว่า

คำถามคือแอปพลิเคชันควรต่ออายุ / เปลี่ยน Token เป็นระยะหรือไม่และถ้าใช่จะทำอย่างไร มันควรจะเป็นแอพมือถือที่ต้องต่ออายุโทเค็นหรือเว็บแอพควรทำแบบอัตโนมัติ?

แต่คำตอบทั้งหมดกำลังเขียนเกี่ยวกับวิธีเปลี่ยนโทเค็นโดยอัตโนมัติ

ฉันคิดว่าการเปลี่ยนโทเค็นเป็นระยะ ๆ โดยโทเค็นไม่มีความหมาย เฟรมเวิร์กที่เหลือสร้างโทเค็นที่มีอักขระ 40 ตัวหากผู้โจมตีทดสอบ 1,000 โทเค็นทุกวินาทีต้องใช้16**40/1000/3600/24/365=4.6*10^7เวลาหลายปีในการรับโทเค็น คุณไม่ควรกังวลว่าผู้โจมตีจะทดสอบโทเค็นของคุณทีละรายการ แม้ว่าคุณจะเปลี่ยนโทเค็นของคุณความน่าจะเป็นในการเดาโทเค็นของคุณก็เหมือนกัน

หากคุณกังวลว่าผู้โจมตีอาจดึงโทเค็นของคุณได้ดังนั้นคุณจึงเปลี่ยนเป็นระยะ ๆ กว่าหลังจากที่ผู้โจมตีได้รับโทเค็นเขายังสามารถเปลี่ยนโทเค็นของคุณได้มากกว่าที่ผู้ใช้จริงจะถูกไล่ออก

สิ่งที่คุณควรทำคือการป้องกันการโจมตี tha จากการเดินทางของผู้ใช้โทเค็นใช้ https

อย่างไรก็ตามฉันแค่บอกว่าการเปลี่ยนโทเค็นโดยโทเค็นไม่มีความหมายการเปลี่ยนโทเค็นด้วยชื่อผู้ใช้และรหัสผ่านบางครั้งก็มีความหมาย บางทีโทเค็นอาจถูกใช้ในสภาพแวดล้อม http (คุณควรหลีกเลี่ยงสถานการณ์เช่นนี้เสมอ) หรือบุคคลที่สามบางราย (ในกรณีนี้คุณควรสร้างโทเค็นประเภทต่างๆใช้ oauth2) และเมื่อผู้ใช้ทำสิ่งที่อันตรายเช่นการเปลี่ยนแปลง การผูกกล่องจดหมายหรือลบบัญชีคุณควรตรวจสอบให้แน่ใจว่าคุณจะไม่ใช้โทเค็นต้นทางอีกต่อไปเนื่องจากผู้โจมตีอาจเปิดเผยโดยใช้เครื่องมือดมกลิ่นหรือ tcpdump


ใช่ตกลงคุณควรได้รับโทเค็นการเข้าถึงใหม่ด้วยวิธีการอื่น (ที่ไม่ใช่โทเค็นการเข้าถึงแบบเก่า) เช่นเดียวกับโทเค็นการรีเฟรช (หรือวิธีเก่าในการบังคับให้เข้าสู่ระบบใหม่ด้วยรหัสผ่านเป็นอย่างน้อย)
BjornW

3

คุณสามารถใช้ประโยชน์จากhttp://getblimp.github.io/django-rest-framework-jwt

ไลบรารีนี้สามารถสร้างโทเค็นที่มีวันหมดอายุ

หากต้องการทำความเข้าใจความแตกต่างระหว่างโทเค็นเริ่มต้นของ DRF และโทเค็นที่ DRF ให้มาโปรดดูที่:

จะสร้าง Django REST JWT Authentication scale ด้วยเว็บเซิร์ฟเวอร์หลายตัวได้อย่างไร


1

ถ้าคุณสังเกตเห็นว่าโทเค็นเป็นเหมือนเซสชันคุกกี้แล้วคุณสามารถติดกับอายุการใช้งานเริ่มต้นของคุกกี้เซสชันใน Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age

ฉันไม่รู้ว่า Django Rest Framework จัดการโดยอัตโนมัติหรือไม่ แต่คุณสามารถเขียนสคริปต์สั้น ๆ ที่กรองสิ่งที่ล้าสมัยออกและทำเครื่องหมายว่าหมดอายุได้


1
การตรวจสอบความถูกต้องของโทเค็นไม่ใช้คุกกี้
29

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

นอกจากนี้อย่าลืมเพิ่มใน DEFAULT_AUTHENTICATION_CLASSES แทน rest_framework.authentication.TokenAuthentication

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.