Token
在移动端开发或者前后端分离开发时,我们会经常用到token(令牌)来验证并保留登录状态,通过向登录接口以post请求方式来发送登录表单获取token,将token保存到本地,并在请求需要身份认证的 url 时,将token放到请求头中就可以直接访问,不用再登录。
token生成的方法有很多种,我们这里不关注具体的生成算法,只是关注功能性实现。
注意事项
需要有一定的django基础,如果一点基础都没有,不建议查看。
使用PyJWT生成token
首先安装pyjwt包
pip install pyjwt
创建Pyjwt_demo项目,在settings.py文件中关闭 csrf 防护。
# 'django.middleware.csrf.CsrfViewMiddleware',
前期工作
创建 account app
python manage.py startapp account
自定义 User 模型
from datetime import datetime, timedelta
import jwt
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser,PermissionsMixin,BaseUserManager
from shortuuidfield import ShortUUIDField
from django.db import models
class UserManager(BaseUserManager):
def _create_user(self,username,password,**kwargs):
if not username:
raise ValueError('请传入用户名!')
if not password:
raise ValueError('请传入密码!')
user = self.model(username=username,**kwargs)
user.set_password(password)
user.save()
return user
def create_user(self,username,password,**kwargs):
kwargs['is_superuser'] = False
return self._create_user(username,password,**kwargs)
def create_superuser(self,username,password,**kwargs):
kwargs['is_superuser'] = True
kwargs['is_staff'] = True
return self._create_user(username,password,**kwargs)
class User(AbstractBaseUser,PermissionsMixin):
email = models.EmailField(unique=True,null=True)
username = models.CharField(max_length=100, unique=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
data_joined = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'username'
objects = UserManager()
def get_full_name(self):
return self.username
def get_short_name(self):
return self.username
# 此处是用户模型中生成token的方法
@property
def token(self):
return self._generate_jwt_token()
def _generate_jwt_token(self):
token = jwt.encode({
'exp': datetime.utcnow() + timedelta(days=1),# 这个地方设置token的过期时间。
'iat': datetime.utcnow(),
'data': {
'username': self.username
}
}, settings.SECRET_KEY, algorithm='HS256')
return token.decode('utf-8')
注册模型
在setting.py中添加
AUTH_USER_MODEL = 'account.User'
模型迁移生成超级用户
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
编写工具类
我们在这里编写两个工具类,一个是我们返回接口的规范,一个是我们在视图函数中验证token是否符合要求的函数。
在account目录下新建一个utils.py 文件。
import jwt
from django.conf import settings
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
UserModel = get_user_model()
def auth_permission_required(perm):
def decorator(view_func):
def _wrapped_view(request, *args, **kwargs):
# 格式化权限
perms = (perm,) if isinstance(perm, str) else perm
if request.user.is_authenticated:
# 正常登录用户判断是否有权限
if not request.user.has_perms(perms):
raise PermissionDenied
else:
try:
auth = request.META.get('HTTP_AUTHORIZATION').split()
except AttributeError:
return result(401,"No authenticate header")
# 用户通过 API 获取数据验证流程
if auth[0].lower() == 'token':
try:
dict = jwt.decode(auth[1], settings.SECRET_KEY, algorithms=['HS256'])
username = dict.get('data').get('username')
except jwt.ExpiredSignatureError:
return result(401,"Token expired")
except jwt.InvalidTokenError:
return result(401,"Invalid token")
except Exception as e:
return result(401,"Can not get user object")
try:
user = UserModel.objects.get(username=username)
except UserModel.DoesNotExist:
return result(401,"User Does not exist")
if not user.is_active:
return result(401,"User inactive or deleted")
# Token 登录的用户判断是否有权限
if not user.has_perms(perms):
return result(401,"PermissionDenied")
else:
return result(401,"Not support auth type")
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
# 接口规范 code message="" data:{}
def result(code=200,message="",data=None,kwargs=None):
json_dict = {"code":code,"message":message,"data":data}
if kwargs and isinstance(kwargs,dict) and kwargs.keys():
json_dict.update(kwargs)
return JsonResponse(json_dict)
编写视图函数
在views.py中编写一下代码
from django.contrib.auth import authenticate
from django.contrib.auth.views import login
from django.http import JsonResponse
from django.shortcuts import render
# Create your views here
from .utils import result, auth_permission_required
from django.views.decorators.http import require_POST
#登录视图分发token,只要用户名和密码正确就分发token
@require_POST
def user_login(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username, password=password)
if user is None:
return result(200, "用户名或者密码错误!")
login(request, user)
# 分发token
token = user.token
return result(200,"登录成功", {"token":token})
#之前定义的验证函数
@auth_permission_required("account.User")
def token_test(request):
return result(200,"认证成功")
编写路由
在urls中根据以下代码进行编写视图路由。
from django.contrib import admin
from django.urls import path
from account import views
urlpatterns = [
path('admin/', admin.site.urls),
path("login/", views.user_login),
path('tokentest/', views.token_test),
]
到这里代码编写的任务就完成了,下一步就是测试,我们使用postman来测试。
使用post,填写好用户名和密码就可以获取token。
在请求头中添加Authentication信息,值为‘token '+token。
如果不加token,就会显示错误。
使用rest-framework生成token
rest-framework是通过生成一张token表的方式来验证token的。
安装rest-framework
pip install djangorestframework
前期工作同上,新建drf_token_demo项目,但是在执行模型迁移之前,我们需要在settings.py中添加如下信息。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'account',
'rest_framework.authtoken', # 一定添加上
]
工具类
为了统一接口规范,我们还是在account文件夹下新建utils.py文件
from django.http import JsonResponse
def result(code=200,message="",data=None,kwargs=None):
json_dict = {"code":code,"message":message,"data":data}
if kwargs and isinstance(kwargs,dict) and kwargs.keys():
json_dict.update(kwargs)
return JsonResponse(json_dict)
编写视图函数
from django.contrib.auth import authenticate
from django.contrib.auth.views import login
from django.shortcuts import render
# Create your views here.
from django.views.decorators.http import require_POST
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
#登录并返回token
@require_POST
def user_login(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username, password=password)
if user is None:
return result(200, "用户名或者密码错误!")
login(request, user)
#把之前已经产生的token删除掉,在添加新的token
token = Token.objects.filter(user=user)
token.delete()
token = Token.objects.create(user=user).key
return result(200,"登录成功", {"token":token})
#这个地方authentication_classes((TokenAuthentication,))是用来验证token是否正确,@permission_classes((IsAuthenticated,))是用来添加权限,只有被认证的用户才能访问,api_view是限制访问的方式。
@api_view(['GET'])
@permission_classes((IsAuthenticated,))
@authentication_classes((TokenAuthentication,))
def token_test(request):
return result(200,"认证成功")
编写路由
from django.contrib import admin
from django.urls import path
from account import views
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', views.user_login),
path('tokentest/', views.token_test),
]
使用和上面相同的方法测试即可,可能生成的token看起来不一样,这个无所谓,注意这个地方还是在token前加上’token ‘的前缀。
token过期问题
在这个地方我们发现没有给token设置期限,这是个很严重的问题,通过缺少自定义化,我们可以通过使用自定义的验证方法实现。
在utils.py中添加如下代码,注意在设置里面设置了时区 USE_TZ = False,如果使用utc这里需要改变。
import jwt
from django.conf import settings
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
class ExpiringTokenAuthentication(BaseAuthentication):
model = Token
def authenticate(self, request):
auth = get_authorization_header(request)
if not auth:
return None
try:
token = auth.decode().split()[-1]
print(token)
except UnicodeError:
msg = ugettext_lazy("无效的Token, Token头不应包含无效字符")
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, cache_user # 这里需要返回一个列表或元组,原因不详
# 缓存获取到此为止
# 下面开始获取请求信息进行验证
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed("认证失败")
if not token.user.is_active:
raise exceptions.AuthenticationFailed("用户被禁用")
# Token有效期时间判断(注意时间时区问题)
# 我在设置里面设置了时区 USE_TZ = False,如果使用utc这里需要改变。
if (datetime.datetime.now() - token.created) > datetime.timedelta(seconds=60):
raise exceptions.AuthenticationFailed('认证信息已过期')
# 加入缓存增加查询速度,下面和上面是配套的,上面没有从缓存中读取,这里就不用保存到缓存中了
if token:
token_cache = 'token_' + key
cache.set(token_cache, token.user, 600)
# 返回用户信息
return token.user, token
def authenticate_header(self, request):
return 'Token'
修改views.py, 添加如下代码。
@api_view(['GET'])
@permission_classes((IsAuthenticated,))
@authentication_classes((ExpiringTokenAuthentication,)) #这是我们自己定义的验证类,需要将其导入进来
def token_test_timestamp(request):
return result(200,"期限token认证成功")
url 里新加一条:
path('tokenteststamp/', views.token_test_timestamp),
访问这个 url 便可以验证token是否过期。
使用django-rest-framework-jwt生成token
我们使用jwt有什么好处呢,目前看来它可以配置token过期时间。
pip install djangorestframework-jwt
前期工作同上,新建drf_jwt_token_demo项目,但是在执行模型迁移之前,我们需要在settings.py中添加如下信息。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'account',
'rest_framework-jwt', # 一定添加上
]
同样新建utils.py,这里不再赘述。
编写视图函数
我们在这个地方使用api_settings来生成token,官方文档有自己自定义的方法,可以自行查看。
from django.contrib.auth import authenticate, login
from django.http import JsonResponse
from django.shortcuts import render
from rest_framework.decorators import permission_classes, authentication_classes, api_view
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.settings import api_settings
# Create your views here.
from account.utils import result
def user_login(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username, password=password)
if user is None:
return result(200, "用户名或者密码错误!")
login(request, user)
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return JsonResponse({"code":200, "token": token})
@api_view(["GET",])
@permission_classes((IsAuthenticated,))
@authentication_classes((JSONWebTokenAuthentication,))# 这里使用JSONWebTokenAuthentication来验证
def token_test(request):
return JsonResponse({"code":200,"message":"successful"})
在settings.py中加入以设置过期时间
JWT_AUTH = {
# 设置token有效期时间
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=60 * 60 * 2)
}
编写路由
from django.contrib import admin
from django.urls import path
from account import views
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', views.user_login),
path('tokentest/', views.token_test),
]
接下来就是验证啦,这里一切同上面验证的方法一致,但是这个地方我们生成的token是JWT类型的,所以token的前缀不再是’token ‘,而是JWT。
你可能会问,这个地方我们能不能像第二种一样自定义验证类呢?完全可以,你可以把第二种里的验证类直接放到这里来使用,但是这个时候就要注意token前缀不再是’JWT‘,而是原来的’token ‘。可以自行验证。
在本篇博文编写过程中参考了以下文章:
Django+JWT 实现 Token 认证
Django 用户登录校验以及接口token校验
适合小白的Django rest_framework Token入门
历经了三天时间,终于把这个概念和源码搞清楚了,这里提醒大家多看官方说明文档,多思考,坚持就是胜利。如果有问题可以私信我或者向我的邮箱lkzhang98@163.com 发送邮件。欢迎关注我的微信公众号Pkill,之后有很多技术文章推送。
三种类型的源代码地址均已上传,地址如下:
https://download.youkuaiyun.com/download/lwuis_/12683638
https://download.youkuaiyun.com/download/lwuis_/12683634
https://download.youkuaiyun.com/download/lwuis_/12683462