RESTful API的令牌身份验证:令牌是否应该定期更改?


115

我正在使用Django和django-rest-framework构建RESTful API 。

作为身份验证机制,我们选择了“令牌身份验证”,而我已经按照Django-REST-Framework的文档进行了实现,问题是,应用程序是否应该定期更新/更改令牌,如果可以,怎么做?是需要续签令牌的移动应用程序,还是应该由网络应用程序自主执行?

最佳做法是什么?

有人在Django REST Framework方面经验丰富,可以提出技术解决方案吗?

(最后一个问题的优先级较低)

Answers:


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

还需要覆盖默认的rest框架登录视图,以便在登录完成后刷新令牌:

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

并且不要忘记修改网址:

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 '18

3
另外,您可以通过定期在cronjob中
淘汰

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作为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端点时出现错误。不知道我在想什么。
Dharmit

2
有趣的解决方案,我将在以后进行测试;目前,您的帖子帮助我走上了正确的道路,因为我只是忘记设置AUTHENTICATION_CLASSES。
规范

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

以为我会用DRY给Django 2.0答案。谷歌Django OAuth ToolKit已经有人为我们建立了。可用pip 、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和scope变量来微调权限。您最终会获得如下信誉:

{
    "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



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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.