Xác thực mã thông báo cho API RESTful: mã thông báo có nên được thay đổi định kỳ không?


115

Tôi đang xây dựng một API RESTful với Django và django-rest-framework .

Như cơ chế xác thực, chúng tôi đã chọn "Xác thực mã thông báo" và tôi đã triển khai nó theo tài liệu của Django-REST-Framework, câu hỏi đặt ra là ứng dụng có nên gia hạn / thay đổi Mã thông báo định kỳ không và nếu có thì làm thế nào? Đó là ứng dụng dành cho thiết bị di động yêu cầu mã thông báo được gia hạn hay ứng dụng web nên thực hiện việc đó một cách độc lập?

Thực hành tốt nhất là gì?

Có ai ở đây đã có kinh nghiệm với Django REST Framework và có thể đề xuất giải pháp kỹ thuật không?

(câu hỏi cuối cùng có mức độ ưu tiên thấp hơn)

Câu trả lời:


101

Việc yêu cầu khách hàng di động gia hạn mã thông báo xác thực của họ theo định kỳ là một thực tiễn tốt. Điều này tất nhiên là tùy thuộc vào máy chủ để thực thi.

Lớp TokenAuthentication mặc định không hỗ trợ điều này, tuy nhiên bạn có thể mở rộng nó để đạt được chức năng này.

Ví dụ:

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

Nó cũng được yêu cầu ghi đè chế độ xem đăng nhập khung phần còn lại mặc định, để mã thông báo được làm mới bất cứ khi nào đăng nhập được thực hiện:

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

Và đừng quên sửa đổi các url:

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

6
Bạn có muốn tạo mã thông báo mới trong ObtainExpiringAuthToken nếu nó đã hết hạn hay không, thay vì chỉ cập nhật dấu thời gian cho cái cũ?
Joar Leth

4
Tạo một mã thông báo mới có ý nghĩa. Bạn cũng có thể tạo lại giá trị của khóa mã thông báo hiện có và sau đó bạn sẽ không phải xóa mã thông báo cũ.
odedfos

Điều gì xảy ra nếu tôi muốn xóa mã thông báo khi hết hạn? Khi tôi get_or_create một lần nữa, mã thông báo mới sẽ được tạo hay dấu thời gian được cập nhật?
Sayok88

3
Ngoài ra, bạn có thể hết hạn thẻ từ bảng bằng cách trục xuất cũ theo định kỳ trong một cronjob (Cần tây đánh bại hoặc tương tự), thay vì chặn xác nhận
BjornW

1
@BjornW Tôi chỉ thực hiện việc loại bỏ và theo ý kiến ​​của tôi, người tích hợp với API (hoặc giao diện người dùng của bạn) có trách nhiệm đưa ra yêu cầu, họ nhận được, "Mã thông báo không hợp lệ", rồi nhấn làm mới / tạo điểm cuối mã thông báo mới
ShibbySham

25

Nếu ai đó quan tâm đến giải pháp đó nhưng muốn có mã thông báo hợp lệ trong một thời gian nhất định thì được thay thế bằng mã thông báo mới, đây là giải pháp hoàn chỉnh (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')
)

dự án của bạn urls.py (trong mảng urlpatterns):

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

yourmodule / verify.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)

Trong cài đặt REST_FRAMEWORK của bạn, hãy thêm ExpiringTokenAuthentication làm lớp Xác thực thay vì TokenAuthentication:

REST_FRAMEWORK = {

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

Tôi gặp lỗi 'ObtainExpiringAuthToken' object has no attribute 'serializer_class'khi cố truy cập vào điểm cuối api. Không chắc chắn những gì tôi đang thiếu.
Dharmit

2
Giải pháp thú vị mà tôi sẽ kiểm tra sau; tại thời điểm này, bài đăng của bạn đã giúp tôi đi đúng hướng vì đơn giản là tôi đã quên đặt AUTHENTICATION_CLASSES.
bình thường

2
Đến bữa tiệc muộn nhưng tôi cần thực hiện một số thay đổi tinh tế để làm cho nó hoạt động. 1) utc_now = datetime.datetime.utcnow () phải là utc_now = datetime.datetime.utcnow (). Replace (tzinfo = pytz.UTC) 2) Trong lớp ExpiringTokenAuthentication (TokenAuthentication): Bạn cần model, self.model = self. get_model ()
Ishan Bhatt

5

Tôi đã thử câu trả lời @odedfos nhưng gặp lỗi gây hiểu nhầm . Đây là câu trả lời tương tự, cố định và nhập khẩu thích hợp.

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

Tôi nghĩ rằng tôi sẽ đưa ra câu trả lời Django 2.0 bằng cách sử dụng DRY. Ai đó đã xây dựng điều này cho chúng tôi, google Django OAuth ToolKit. Có sẵn với pip , pip install django-oauth-toolkit. Hướng dẫn thêm mã thông báo ViewSets với bộ định tuyến: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html . Nó tương tự như hướng dẫn chính thức.

Vì vậy, về cơ bản OAuth1.0 đã được bảo mật hơn của ngày hôm qua, đó là TokenAuthentication. Để nhận được các mã thông báo hết hạn ưa thích, OAuth2.0 đang là cơn thịnh nộ hiện nay. Bạn nhận được một biến AccessToken, RefreshToken và phạm vi để tinh chỉnh các quyền. Bạn kết thúc với các khoản tín dụng như thế này:

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

4

Tác giả hỏi

câu hỏi đặt ra là ứng dụng có nên gia hạn / thay đổi Token định kỳ không và nếu có thì làm thế nào? Đó là ứng dụng dành cho thiết bị di động yêu cầu mã thông báo được gia hạn hay ứng dụng web nên thực hiện việc đó một cách độc lập?

Nhưng tất cả các câu trả lời đều viết về cách tự động thay đổi mã thông báo.

Tôi nghĩ rằng thay đổi mã thông báo định kỳ bằng mã thông báo là vô nghĩa. Khung còn lại tạo mã thông báo có 40 ký tự, nếu kẻ tấn công kiểm tra 1000 mã thông báo mỗi giây, thì cần 16**40/1000/3600/24/365=4.6*10^7nhiều năm để lấy mã thông báo. Bạn không nên lo lắng rằng kẻ tấn công sẽ kiểm tra từng mã thông báo của bạn. Ngay cả khi bạn đã thay đổi mã thông báo của mình, xác suất đoán mã thông báo của bạn là như nhau.

Nếu bạn lo lắng rằng có thể những kẻ tấn công có thể lấy được mã thông báo của bạn, vì vậy bạn thay đổi nó theo định kỳ, sau khi kẻ tấn công lấy được mã thông báo, anh ta cũng có thể thay đổi mã thông báo của bạn, hơn là người dùng thực bị đuổi.

Điều bạn thực sự nên làm là ngăn kẻ tấn công lấy mã thông báo của người dùng của bạn, hãy sử dụng https .

Nhân tiện, tôi chỉ nói rằng thay đổi mã thông báo bằng mã thông báo là vô nghĩa, thay đổi mã thông báo bằng tên người dùng và mật khẩu đôi khi có ý nghĩa. Có thể mã thông báo được sử dụng trong một số môi trường http (bạn nên luôn tránh loại trường hợp này) hoặc bên thứ ba nào đó (trong trường hợp này, bạn nên tạo loại mã thông báo khác, sử dụng oauth2) và khi người dùng đang làm một số điều nguy hiểm như thay đổi ràng buộc hộp thư hoặc xóa tài khoản, bạn nên chắc chắn rằng bạn sẽ không sử dụng mã thông báo gốc nữa vì nó có thể đã bị kẻ tấn công tiết lộ bằng cách sử dụng công cụ hít hoặc tcpdump.


Có, đồng ý, bạn sẽ nhận được mã thông báo truy cập mới bằng một số phương tiện khác (thay vì mã thông báo truy cập cũ). Giống như với mã thông báo làm mới (hoặc cách cũ để buộc đăng nhập mới với mật khẩu ít nhất).
BjornW


1

Nếu bạn nhận thấy rằng mã thông báo giống như cookie phiên thì bạn có thể bám vào thời gian tồn tại mặc định của cookie phiên trong Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age .

Tôi không biết Django Rest Framework có xử lý điều đó tự động hay không nhưng bạn luôn có thể viết một đoạn script ngắn lọc ra những cái lỗi thời và đánh dấu chúng là hết hạn.


1
Xác thực mã thông báo không sử dụng cookie
s29

0

Tôi chỉ nghĩ rằng tôi sẽ thêm của tôi vì điều này rất hữu ích cho tôi. Tôi thường đi với phương pháp JWT nhưng đôi khi một cái gì đó như thế này tốt hơn. Tôi đã cập nhật câu trả lời được chấp nhận cho django 2.1 với các mục nhập phù hợp ..

xác thực.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

chỉ để tiếp tục thêm vào câu trả lời @odedfos, tôi nghĩ rằng đã có một số thay đổi đối với cú pháp, vì vậy mã của ExpiringTokenAuthentication cần một số điều chỉnh:

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

Ngoài ra, đừng quên thêm nó vào DEFAULT_AUTHENTICATION_CLASSES thay vì rest_framework.authentication.TokenAuthentication

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.