Django实现API Token认证机制 --- Django+restframework+JWT

Django 实现 API Token认证机制

项目地址:https://github.com/ylpxzx/django_jwt_example

Session鉴权与Token鉴权的区别
传统session认证

HTTP协议是无状态的,而session的主要目的就是给无状态的HTTP协议添加状态保持,通常在浏览器作为客户端的情况下比较通用,需要在服务端去保留用户的认证信息或者会话信息。
流程:

  1. 注册账号
  2. 登录页面输入账号密码提交表单后,发送请求给服务器
  3. 服务器对账号密码进行验证鉴权,验证鉴权通过后,把用户信息记录在服务器端(django_session表中),同时返回给浏览器一个sessionid用来唯一标识这个用户
  4. 浏览器将sessionid保存在cookie中,之后浏览器的每次请求都一并将sessionid发送给服务器
  5. 服务器根据sessionid与记录的信息做对比以验证身份。

基于session认证所显露的问题:

  • 每个用户经过认证之后,服务端都要做一次记录(保存他的会话状态),以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
  • 扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求必须请求同一台服务器才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
Token认证

Token的主要目的是为了鉴权,同时又不需要考虑CSRF防护以及跨域的问题,多用于第三方提供API的情况下,客户端请求无论是浏览器发起还是其他程序发起都能很好的支持。目前基于Token的鉴权机制几乎已经成了前后端分离架构或者对外提供API访问的鉴权标准。
相较于session的区别:
基于token的鉴权机制类似于http协议也是无状态的,它**不需要在服务端去保留用户的认证信息或者会话信息。**这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录,为应用的扩展提供了便利。
流程:

  1. 注册账号
  2. 用户使用用户名和密码请求服务器
  3. 服务器进行验证用户的信息
  4. 服务器通过验证后,发送给用户一个token
  5. 客户端存储token,并在每次请求时在请求头附上这个token值
  6. 服务端验证token值,并返回数据
Django 用于实现Token认证机制的第三方库
pip install aliyun-python-sdk-core-v3 # 阿里云短信服务sdk
pip install django
pip install djangorestframework
pip install djangorestframework-jwt
实现步骤

本例子的项目名为:login_jwt
应用名为:users

扩展系统AbstractUser用户表
  • users/models.py
    自定义的phone_numbers字段用于后续短信验证,相关阿里云短信服务实现可参考上篇文章
from django.db import models
from django.contrib.auth.models import AbstractUser

#继承AbstractUser,对原有的User表进行扩展,记得在setting中修改为AUTH_USER_MODEL = 'users.LoginUser'
class LoginUser(AbstractUser):
    '''
    用户表
    '''
    phone_numbers = models.CharField(verbose_name='手机号', unique=True,max_length=11, default='')

    def __str__(self):
        return self.username
  • login_jwt/settings.py
AUTH_USER_MODEL = 'users.LoginUser'  # 扩展系统的用户表后记得添加此行
定义序列器

用于对提交数据进行序列化和验证

  • 在名为users的应用app创建serializers.py文件
import re
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from django.contrib.auth.hashers import make_password
from .models import LoginUser
from django.core.cache import cache

def re_phone(phone):
	# 检验手机号是否符合标准格式
    ret = re.match(r"^1[1-8]\d{9}$", phone)
    if ret:
        return True
    return False

class SmsSerializer(serializers.ModelSerializer):
	'''
	在获取短信验证码前,需对提交的phone字段进行检查,查看是否已注册或格式不正确
	'''
    phone = serializers.CharField(required=True)
    class Meta:
        model = LoginUser
        fields = ('phone',)
    def validate_phone(self,phone):
        '''
        手机号验证
        :return:
        '''
        # validate_phone格式为validate_字段,检验指定字段
        if LoginUser.objects.filter(phone_numbers=phone).count():
            raise ValidationError('手机号码已经注册')
        if not re_phone(phone):
            raise ValidationError('手机号码格式错误')
        return phone

class RegisterSerializer(serializers.ModelSerializer):
	'''
	手机获取到验证码后,可进行注册操作,该序列器规定了所要提交的字段fields,并对提交数据进行检查
	'''
    phone_numbers = serializers.CharField(required=True)
    pwd2 = serializers.CharField(max_length=256,min_length=4,write_only=True)
    code = serializers.CharField(required=True)

    class Meta:
        model = LoginUser
        # 'username', 'password'是系统用户表中已经存在的,系统会自动对用户输入的username进行检查
        fields = ('username', 'password', 'pwd2', 'phone_numbers', 'code')

    def validate(self, attrs):
    	# validate对所有字段attrs进行自定义检验
        print(attrs['code'])
        if not re_phone(attrs['phone_numbers']):
            raise ValidationError('手机号码格式错误')
            
		# 获取redis的数据
        sms_code = cache.get(attrs['phone_numbers'])
        if str(sms_code) != attrs['code']:
            raise ValidationError('验证码错误或过期')
        if attrs['pwd2'] != attrs['password']:
            raise ValidationError('两次密码输入不一致')
        del attrs['pwd2']
        del attrs['code']
        attrs['password'] = make_password(attrs['password'])
        return attrs

class OrderSerializer(serializers.Serializer):
	'''
	该序列器用于测试
	'''
    title = serializers.CharField()
    name = serializers.CharField()
定义api路径
  • 自行创建urls.py文件:users/urls.py
from django.conf.urls import url
from .views import *
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
        url(r'^sms/',SmsView.as_view()),  # 短信发送api
        url(r'^register/',RegisterView.as_view()),  # 账号注册api
        url(r'^index/',Order.as_view()),  # 测试api
        url(r'^api-jwt-auth/',obtain_jwt_token),  # jwt的认证接口(路径可自定义任意命名)
]
  • login_jwt/urls.py
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('users.urls')),
]
定义APIView视图
  • users/views.py
import random
from rest_framework.views import APIView
from .serializers import *
from rest_framework.response import Response
# from .models import UserToken
from rest_framework_jwt.settings import api_settings
from conf.aliyun_api import AliYunSms

class SmsView(APIView):
    '''
    发送验证码
    '''
    authentication_classes = []  # 因为后续会在settings.py中设置全局鉴权,而且发送短信验证码不需要登录认证,所以这里设置为[],跳过鉴权
    permission_classes = []  # 同理
    def post(self,request, *args, **kwargs):
        serializer = SmsSerializer(data=request.data)
        if serializer.is_valid():
            code = (random.randint(1000, 100000))

            response = {
                'msg':'手机号格式正确,已发送验证码,注意查收',
                'next_url':{
                    'url':'api/register',
                    'methond':'POST',
                    'form-data':{
                        'username':'用户名',
                        'phone':'手机号',
                        'password':'密码',
                        'password2':'确认密码',
                        'code':'验证码'
                    }
                }
            }
            phone = serializer.data['phone']
            response['phone'] = phone
            response['code'] = code  # 记得后续删除该行,避免泄露验证码,目前只用于方便查看验证码

            cache.set(phone, code, 150)

            # # 发送短信验证
            # params = "{'code':%d}" % code
            # sms_obj = AliYunSms(phone, params)
            # res_obj = sms_obj.send()
            # print('发送结果:',res_obj)

            return Response(response,status=200)
        return Response(serializer.errors,status=400)


class RegisterView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self,request, *args, **kwargs):
        serializer = RegisterSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()  # 保留注册数据,即在数据库中创建了用户
      
            response = {
                'msg':'用户注册成功',
                'next_url':{
                    'url':'api/api-jwt-auth/',
                    'form-data':{
                        'username': '用户名',
                        'password': '密码'
                    }
                }
            }
            return Response(response,status=200)
        return Response(serializer.errors,status=400)


class Order(APIView):
	# 访问Order视图时时,要加上请求头,请求头的键为:authorization,值为:jwt空格token
    def get(self, request):
        ret = {'code': 1000, 'msg': '成功GET进来了', 'data': None}
        ret['data'] = '欢迎使用本系统'
        return Response(ret)

    def post(self,request):
        order = OrderSerializer(data=request.data)
        if order.is_valid():
            print(order)
            ret = {'code': 1000, 'msg': '成功POST进来了', 'data': order.data}
            return Response(ret)
        return Response(order.errors,status=400)
settings.py配置
  • 配置INSTALLED_APPS
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',  # 添加该行
    'users',
]
  • 配置Redis缓存
    用于验证码的存活期不长,所以存入Redis,保存时间过期后删除。
# 配置redis缓存,短时间存储手机验证码
CAHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": 'redis://127.0.0.1:6379',
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            # "PASSWORD": "密码",
            "DECODE_RESPONSES":True
        }
    },
}
  • 全局认证配置
# 全局认证
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
          # 设置访问权限为只读
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
          # 设置访问权限为必须是用户
        'rest_framework.permissions.IsAuthenticated',
),

    'DEFAULT_AUTHENTICATION_CLASSES': (
    	# 自上而下认证
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

import datetime
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),  # 设置 JWT Token 的有效时间
    'JWT_AUTH_HEADER_PREFIX': 'JWT',  # 设置 请求头中的前缀
}
数据库迁移
python manage.py makemigrations
python manage.py migrate
运行
python manage.py runserver
请求过程演示

采用postman对api进行请求测试
将下面示例图中的请求链接{{url}}替换为http://127.0.0.1:8000/

  • 短信发送api
    发送成功后,会获取到code字段,用于注册api注册验证。在生产环境下,记得在返回的数据中去除该字段
    在这里插入图片描述
  • 注册api
    在这里插入图片描述
  • 获取token
    在这里插入图片描述
  • 测试:请求头有无加token的情况
  1. 请求头不加token,请求失败
    在这里插入图片描述
  2. 请求头附加token
    格式
    key:authorization
    value:jwt空格token
    在这里插入图片描述
  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狼性书生

谢谢鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值