Django使用Pyjwt、rest-framework、rest-framework-jwt 生成token

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.csdn.net/download/lwuis_/12683638
https://download.csdn.net/download/lwuis_/12683634
https://download.csdn.net/download/lwuis_/12683462

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值