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)

 

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