Аутентифікація токена для API RESTful: чи слід маркер періодично змінювати?


115

Я будую RESTful API з Django та django-rest-Framework .

В якості механізму аутентифікації ми вибрали "Token Authentication", і я вже реалізував його, слідуючи документації 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, якщо термін його дії минув, а не просто оновити часову позначку для старої?
Жоар Лет

4
Створювати новий маркер має сенс. Ви також можете відновити значення існуючого ключа лексем, і тоді вам не доведеться видаляти старий маркер.
odedfos

Що робити, якщо я хочу видалити маркер після закінчення терміну дії? Коли я get_or_create знову, буде створено новий маркер або оновиться часова марка?
Сайок88

3
Крім того, ви можете вилучити жетони зі столу, періодично виселяючи старі з під час роботи (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')
)

ваш проект 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:

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 ()
Ішан Бхатт

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-toolkit. Інструкції щодо додавання маркера 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

- спитав автор

питання полягає в тому, чи слід програму періодично поновлювати / змінювати маркер і якщо так, то як? Чи повинен мобільний додаток, який вимагає оновлення маркера, або веб-додаток повинен робити це автономно?

Але всі відповіді написані про те, як автоматично змінити маркер.

Я вважаю, що періодично змінювати маркер на маркер безглуздо. Решта рамки створюють маркер, що містить 40 символів, якщо зловмисник тестує 1000 маркерів щосекунди, 16**40/1000/3600/24/365=4.6*10^7для отримання жетону потрібні роки. Ви не повинні турбуватися, що зловмисник перевірятиме ваш маркер по черзі. Навіть ви змінили маркер, ймовірність здогадатися, що ви бачите, така ж.

Якщо ви переживаєте, що, можливо, зловмисники можуть отримати токен, тож ви періодично його міняєте, ніж після того, як зловмисник отримає маркер, він також може змінити ваш маркер, ніж реального користувача виганяють.

Що ви дійсно повинні зробити, це не допустити, щоб зловмисник отримував токен вашого користувача, використовуйте https .

До речі, я просто кажу, що зміна токена за токеном є безглуздим, зміна маркера за іменем користувача та паролем іноді має значення. Можливо, маркер використовується в якомусь середовищі http (ви завжди повинні уникати подібної ситуації) або на сторонній стороні (у цьому випадку вам слід створити інший тип токена, використовувати oauth2) і коли користувач робить якусь небезпечну річ, наприклад зміну прив’язуючи поштову скриньку або видаляючи обліковий запис, ви повинні переконатися, що більше не будете використовувати маркер початкового коду, оскільки це може бути виявлено зловмисником за допомогою інструментів sniffer або tcpdump.


Так, погодьтеся, ви повинні отримати новий маркер доступу іншим способом (ніж старий маркер доступу). Як і за допомогою маркера оновлення (або старого способу змусити новий логін принаймні з паролем).
BjornW

3

Ви можете використовувати http://getblimp.github.io/django-rest-framework-jwt

Ця бібліотека здатна генерувати маркер, у якого є термін дії

Щоб зрозуміти різницю між маркером за замовчуванням DRF та маркером, наданим DRF, подивіться на:

Як зробити шкалу аутентифікації Django REST JWT з багатоповерховими веб-серверами?


1

Якщо ви помітили, що маркер схожий на файли cookie сеансу, ви можете дотримуватися стандартного життя файлів cookie сеансу в Django: https://docs.djangoproject.com/en/1.4/ref/settings/#session-cookie-age .

Я не знаю, чи Django Rest Framework обробляє це автоматично, але ви завжди можете написати короткий сценарій, який фільтрує застарілі та позначає їх як закінчені.


1
Аутентифікація Token не використовує файли cookie
s29

0

Я просто подумав, що я додам свою, оскільки це було корисно для мене Я зазвичай йду методом JWT, але іноді щось подібне краще. Я оновив прийняту відповідь на джанго 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.