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