原文链接: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)