Django-rest-framework Token不過期解決方法

原文鏈接:https://www.jianshu.com/p/e0a206212df4

簡書上找到的鏈接,使用時有點問題,修改了一下,保存記錄,

django-rest-framework 有一套默認的token驗證機制dfs token驗證 具體用法不再細講了,官方文檔寫得很清楚。

這個token驗證機制存的token,一旦生成就保持不變。這樣就引發一些問題,萬一某人拿到你的token不就爲所欲爲了嗎,就像別人拿到你的密碼一樣。

解決方案: 給token設置過期時間,超過存活時間,這段token不再具有驗證功能,每次用戶重新登入,刷新token(這段新token的有存活時間)。這樣,重新登入後,你的token更新了,某些居心不良的人即便拿着之前搶來的token也沒用。stackoverflow上已經有了token過期時間的討論。

1. 首先,看下TokenAuthentication模塊的源碼如下:

class TokenAuthentication(BaseAuthentication):
    """
    Simple token based authentication.

    Clients should authenticate by passing the token key in the "Authorization"
    HTTP header, prepended with the string "Token ".  For example:

        Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
    """

    keyword = 'Token'
    model = None

    def get_model(self):
        if self.model is not None:
            return self.model
        from rest_framework.authtoken.models import Token
        return Token

    """
    A custom token model may be used, but must have the following properties.

    * key -- The string identifying the token
    * user -- The user to which the token belongs
    """

    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != self.keyword.lower().encode():
            return None

        if len(auth) == 1:
            msg = _('Invalid token header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid token header. Token string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            token = auth[1].decode()
        except UnicodeError:
            msg = _('Invalid token header. Token string should not contain invalid characters.')
            raise exceptions.AuthenticationFailed(msg)

        return self.authenticate_credentials(token)

     def authenticate_credentials(self, key):    # key=前端在請求頭傳過來的Token
         model = self.get_model()     # 獲取Token模塊
         try:
             token = model.objects.select_related('user').get(key=key)   # 通過key獲取Token
         except model.DoesNotExist:
             raise exceptions.AuthenticationFailed(_('Invalid token.'))   # 如果沒有就報錯

         if not token.user.is_active:
             raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))  # 如果用戶沒有激活也報錯

         return (token.user, token)

     def authenticate_header(self, request):
         return self.keyword  # 然後把Token和登錄的用戶返回給View

    def authenticate_header(self, request):
        return self.keyword

上面有註解的地方是關鍵。 過期驗證,就相當於多加一個判斷,只要繼承該類,然後重寫authenticate_credentials方法即可。以下是我實現的例子,以auth.py保存。

import pytz
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _

import datetime

from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework.authtoken.models import Token


def get_authorization_header(request):
    """
    Return request's 'Authorization:' header, as a bytestring.

    Hide some test client ickyness where the header can be unicode.
    """
    auth = request.META.get('HTTP_AUTHORIZATION', b'')
    if isinstance(auth, type('')):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth


class ExpiringTokenAuthentication(BaseAuthentication):
    model = Token

    def authenticate(self, request):
        auth = get_authorization_header(request)

        if not auth:
            return None
        try:
            token = auth.decode()
        except UnicodeError:
            msg = _('Invalid token header. Token string should not contain invalid characters.')
            raise exceptions.AuthenticationFailed(msg)

        return self.authenticate_credentials(token)

    def authenticate_credentials(self, key):

        token_cache = 'token_' + key
        cache_user = cache.get(token_cache)
        if cache_user:
            return (cache_user.user, cache_user)  # 首先查看token是否在緩存中,若存在,直接返回用戶

        try:
            token = self.model.objects.get(key=key)
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed('認證失敗')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('用戶被禁止')

        utc_now = datetime.datetime.utcnow()
        if token.created < (utc_now - datetime.timedelta(hours=24 * 14)).replace(tzinfo=pytz.timezone('UTC')):  # 設定存活時間 14天
            raise exceptions.AuthenticationFailed('認證信息過期')

        if token:
            token_cache = 'token_' + key
            cache.set(token_cache, token, 24 * 7 * 60 * 60)  # 添加 token_xxx 到緩存

        return (cache_user.user, cache_user)

    def authenticate_header(self, request):
        return 'Token'

還要配置settings文件

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'api_test.utils.auth.ExpiringTokenAuthentication'
    ),
}

我的login函數是這樣寫的

class ObtainAuthToken(APIView):
    throttle_classes = ()
    authentication_classes = ()
    permission_classes = ()
    parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
    renderer_classes = (renderers.JSONRenderer,)
    serializer_class = AuthTokenSerializer
    schema = AutoSchema(
        manual_fields=[
            coreapi.Field(name='username', required=True, location='', description='用戶名',
                          schema=coreschema.String(), type="string", example="admin"),
            coreapi.Field(name='password', required=True, location='', description='登錄密碼',
                          schema=coreschema.String(), type="string", example="jiedian1234"),
        ]
    )

    def post(self, request):
        """
        用戶登錄
        """
        serializer = self.serializer_class(data=request.data,
                                           context={"request": request})

        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data["user"]
        try:
            token = Token.objects.get(user=user)
            token.delete()
        except ObjectDoesNotExist:
            pass
        Token.objects.create(user=user)
        token = Token.objects.get(user=user)
        token_cache = 'token_' + token.key
        cache.set(token_cache, token, 24 * 7 * 60 * 60)
        # token, created = Token.objects.get_or_create(user=user)
        data = TokenSerializer(token).data
        return JsonResponse(data=data, code_msg=response.SUCCESS)

退出登錄的視圖,包括視圖token驗證是這樣寫的

class LoginOut(APIView):
    authentication_classes = (ExpiringTokenAuthentication,)
    permission_classes = (permissions.IsAuthenticated,)
    schema = AutoSchema(
        manual_fields=[
            coreapi.Field(name='Authorization', required=True, location='header', description='token',
                          schema=coreschema.String(), type="string", example="Token string"),
        ]
    )

    def post(self, request):
        """
        退出登錄
        """
        token = Token.objects.get(user=request.user)
        token_cache = 'token_' + token.key
        cache.delete_pattern(token_cache)
        token.delete()
        return JsonResponse(code_msg=response.SUCCESS)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章