drf续集之 - 自动生成接口文档、JWT

DRF - 自动生成接口文档

1. 安装coreapi

pip install coreapi

2. 配置

models.py

from django.db import models


class Book(models.Model):
    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    publisher = models.CharField(max_length=32)

serializer.py

from rest_framework import serializers
from app01.models import Book


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

views.py

from rest_framework.viewsets import ModelViewSet
from app01.serializer import BookSerializer
from app01.models import Book


class BookView(ModelViewSet):
    '''
    get:
    返回所有图书信息.

    post:
    新建图书.

    delete:
    根据输入的id删除图书

    put:
    根据输入的id修改图书
    '''
    queryset = Book.objects.all()
    serializer_class = BookSerializer

urls.py

from django.contrib import admin
from django.urls import path, re_path
from app01 import views
from rest_framework.documentation import include_docs_urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('book/', views.BookView.as_view({'get': 'list', 'post': 'create'})),
    re_path('^book/(?P<pk>\d+)', views.BookView.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
    path('docs/', include_docs_urls(title='图书管理API'))
]

在这里插入图片描述
在这里插入图片描述

DRF - JWT

一:JWT介绍

1. 简介

JWT的全称为JSON Web Token,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。

  • token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
  • JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

2. 构成

JWT的本质就是token,它主要有三部分组成,分别是头部(header)、荷载(payload)主题部、以及签证(signature)。

  • 前两部分都是由base64进行编码(可以反解码),后一部分是不可反解的加密,由前两部分base64的结果加密(hash256)后组成。
  • 各个部分之间由.来分割

实例:

ewogICAgImlkIjoiMDA3IiwKICAgICJuYW1lIjoiRGFya2VyIiwKICAgICJhZ2UiOiIxOCIsCn0=.eyJzdWIiOiIxMjMzMjExMjM0NTY3IiwibmFtZSI6IkRhcmtlciIsImFkbWluIjp0cnVlfQ==.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

重点:JWT和语言、框架无关


二:JWT的组成部分

header

header中,一般携带着2部分信息(声明):

  1. 声明类型:这里用的是JWT
  2. 声明加密的算法:通常直接使用 HMAC SHA256
  • 除此之外,也可以添加其他声明:比如说,添加公司名称等信息

自定义headerJSON格式:

{
    "type": "JWT",
    "encode_method": "HASH256"
}

base64对其进行编码,得到JWT中的header部分:

ewogICAgInR5cGUiOiJKV1QiLAogICAgImVuY29kZV9tZXRob2QiOiJIQVNIMjU2Igp9

payload

荷载部作为JWT三部分中的第二部分,都是存放有效信息。

它可以存放三种类型的有效信息:

  1. 标准中注册的声明
  2. 公共的声明
  3. 私有的声明

标准中注册的声明(建议但不强制使用):

荷载部位的key描述
issJWT签发者(服务端)
subJWT所面向的用户
aud接收JWT的一方
expJWT的过期时间,该时间必须大于签发时间
nbf再某一时间段之前,该JWT不可用
jtiJWT的唯一身份标识,主要用作一次性token,回避时序攻击

公共的声明:

  • 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密

私有的声明:

  • 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息

自定义payloadJSON格式:

{
    "id": 1024,
    "sub": "1233211234567",
    "name": "Darker",
    "admin": true
}

base64对其进行编码,得到JWT中的payload部分:

ewogICAgImlkIjogMTAyNCwKICAgICJzdWIiOiAiMTIzMzIxMTIzNDU2NyIsCiAgICAibmFtZSI6ICJEYXJrZXIiLAogICAgImFkbWluIjogInRydWUiCn0=

signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成

  1. header (base64后的)
  2. payload (base64后的)
  3. secret
  • 这个部分需要base64加密后的headerbase64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。
signature = hashlib.sha256()
signature.update(header_payload_result)
salt = 'llnb'
signature.update(salt.encode("utf-8"))    # 加盐
signature_result = signature.hexdigest()    # 获得结果

JWT第三部分结果

b465493b91a918d9d957f61f8222f0f6dcbb58d428977543be33b1ee5067f228

三:jwt认证算法:签发与校验

  1. JWT分成3部分:头(header)、体(payload)、签名(signature)

  2. 头(header)和体(payload)是可逆加密,让服务器可以反解出user对象;签名(signature)是不可逆加密,保证整个token的安全性的

  3. 头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法

  4. 头(header)中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息

    {
        "company": "公司信息",
        "type": "JWT",
        "encode_method": "HASH256"
        ...
    }
    
  5. 体(payload)中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间
    {
        "id": 1024,
        "sub": "1233211234567",
        "name": "Darker",
        "admin": true
    }
    
  6. 签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密
    {
        "head": "头的加密字符串",
        "payload": "体的加密字符串",
        "secret_key": "安全码"
    }
    

签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token

  1. 用基本信息存储json字典,采用base64算法加密得到 头字符串
  2. 用关键信息存储json字典,采用base64算法加密得到 体字符串
  3. 用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串
  • 账号密码就能根据User表得到user对象,形成的三段字符串 用 . 拼接成token返回给前台

校验:根据客户端带token的请求 反解出 user 对象

  1. token. 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
  2. 第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且是同一设备来的
  3. 再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户

drf项目的jwt认证开发流程(重点)

  1. 用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies
  2. 校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户
  • 注:登录接口需要做 认证 + 权限 两个局部禁用

四:制作JWT

Python进行演示,如何创建一个JWT:

  1. 进行header头部声明
  2. 进行payload荷载声明,不要存放密码等敏感信息,因为可通过反解出来(也可以存放过期时间等)
  3. 将头部与荷载进行.拼接
  4. 将头部与荷载的信息与盐进行hash256加密(通常加密方式都是在alg中声明的),得到签证/签名
  5. 通过.拼接出jwt
import hashlib
import base64
import json

# 第一步:进行header头部声明
header = {
    "type": "JWT",
    "encode_method": "HASH256"
}

header_result = base64.b64encode(json.dumps(header).encode("utf-8"))

# 第二步:进行payload荷载声明,不要存放密码等敏感信息,因为可通过反解出来(也可以存放过期时间等)
payload = {
    "id": 1024,
    "sub": "1233211234567",
    "name": "Darker",
    "admin": "true"
}

payload_result = base64.b16encode(json.dumps(payload).encode("utf-8"))

# 第三步:将头部与荷载进行 . 拼接
header_payload_result = header_result + b"." + payload_result

# 第四步:将头部与荷载的信息与盐进行hash256加密(通常加密方式都是在alg中声明的),得到签证/签名
signature = hashlib.sha256()
signature.update(header_payload_result)
salt = 'llnb'
signature.update(salt.encode("utf-8"))  # 加盐
signature_result = signature.hexdigest()  # 获得结果

# 第五步:通过 . 拼接出jwt
jwt = header_payload_result + b"." + signature_result.encode("utf-8")
print(jwt)

最终结果

eyJ0eXBlIjogIkpXVCIsICJlbmNvZGVfbWV0aG9kIjogIkhBU0gyNTYifQ==.7B226964223A20313032342C2022737562223A202231323333323131323334353637222C20226E616D65223A20224461726B6572222C202261646D696E223A202274727565227D.b465493b91a918d9d957f61f8222f0f6dcbb58d428977543be33b1ee5067f228

五:JWT的认证流程

登录

用户携带用户名和密码 -> 登录系统 -> 校验通过 -> 生成1个Token(3部分) -> 返回给用户 -> 登录功能完成

访问需要登录的接口

用户携带Token去访问登录接口 -> 后端拿到Token -> 截取出Token中的headerpayload -> 用一样的加密方式和密码得到一个signature -> 和该Tokensignature进行比较

JWT的验证流程

在这里插入图片描述

原生token验证的流程:

在这里插入图片描述

六:JWT的优势

如果要体现JWT的优势,则需要与cookie以及session做对比。

  • cookie的劣势:
    主要是存储时不安全,所有数据存放只用户本地,一旦被窃取就可以伪造登录。

  • session的劣势主要有三点:

    1. 数据存放至服务器,占用服务器资源。
    2. 每次用户登录成功后,都需要向数据库中写入session,速度缓慢。
    3. 对于集群式的部署,如果存储session的数据库不一致,则会是个大麻烦,因为用户如果接入了不同的服务器,则意味着写入的session也在不同的数据库中。这会导致用户的session在不同数据库中会存在多次写入的问题。
  • 了解了cookie以及session的劣势后,jwt的优势就显而易见。

    1. 用户数据存放至本地,但必须要与服务端存储的盐进行对比一致后才认证成功。
    2. 不需要有数据库写入的操作。
    3. 集群式部署时也没有任何问题,前提是每个服务器的盐都一样。

七: JWT的安装与使用

安装

  1. 官网
    http://getblimp.github.io/django-rest-framework-jwt/
  2. pip安装
    pip install djangorestframework-jwt
    

快速使用(默认使用authuser表)

  1. 在默认authuser表中创建一个用户
  2. 在路由中配置
    from rest_framework_jwt.views import obtain_jwt_token
    urlpatterns = [
        path('login/', obtain_jwt_token),
        ...
    ]
    
  3. postman向这个地址发送post请求,携带用户名、密码,登陆成功就会返回token
  4. obtain_jwt_token本质也是一个视图类,继承了APIView
    • 通过前端传入的用户名密码,校验用户,如果校验通过,生成token,返回
    • 如果校验失败,返回错误信息
  5. 如果用户携带了token,并且配置了JSONWebTokenAuthentication,从request.user就能拿到当前登录用户,如果没有携带,当前登录用户就是匿名用户
  6. 前端要发送请求,携带jwt,格式必须如下
    • token放到请求头中,key为:Authorization
    • value必须为:jwt
    eyJ0eXBlIjogIkpXVCIsICJlbmNvZGVfbWV0aG9kIjogIkhBU0gyNTYifQ==.7B226964223A20313032342C2022737562223A202231323333323131323334353637222C20226E616D65223A20224461726B6572222C202261646D696E223A202274727565227D.b465493b91a918d9d957f61f8222f0f6dcbb58d428977543be33b1ee5067f228
    
用户登录以后才能访问某个接口

jwt模块内置了认证类,拿过来局部配置就可以

class OrderView(APIView):
    # 只配它不行,不管是否登录,都能范围,需要搭配一个内置权限类
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated,]
    def get(self, request):    
        print(request.user.username)
        return Response('订单的数据')
用户未登录,也能访问某个接口
class OrderView(APIView):
    # 只配它不行,不管是否登录,都能范围,需要搭配一个内置权限类
    authentication_classes = [JSONWebTokenAuthentication, ]
    def get(self, request):
        print(request.user.username)
        return Response('订单的数据')
用户未登录,也能访问某个接口
class OrderView(APIView):
    # 只配它不行,不管是否登录,都能范围,需要搭配一个内置权限类
    authentication_classes = [JSONWebTokenAuthentication, ]
    def get(self, request):
        print(request.user.username)
        return Response('订单的数据')

八:自定义基于jwt的认证类

实现基于jwt的认证类,通过认证,才能继续访问,通不过认证就返回错误

编写1个类

from rest_framework_jwt.exceptions import AuthenticationFailed

class JwtAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        # 认证逻辑()
        # token信息可以放在请求头中,请求地址中
        # key值可以随意叫
        # token=request.GET.get('token')
        token=request.META.get('HTTP_Authorization'.upper())
        # 校验token是否合法
        try:
            payload = jwt_decode_handler(token)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('过期了')
        except jwt.DecodeError:
            raise AuthenticationFailed('解码错误')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('不合法的token')
        user = self.authenticate_credentials(payload)
        return (user, token)

全局使用

settings.py

REST_FRAMEWORK = {
    authentication_classes = [JwtAuthentication, ],
}

局部使用

views.py

class Mine(APIView):
    authentication_classes = [JwtAuthentication, ]
    ...

全局使用+局部禁用

settings.py

REST_FRAMEWORK = {
    authentication_classes = [JwtAuthentication, ],
}

views.py

class Mine(APIView):
    authentication_classes = []
    ...

九:控制登录接口返回的数据格式

1. 控制登录接口返回的数据格式如下

{
    "code":  100,
    "msg": "登录成功",
    "token""xxxxxx.xxxxxxx.xxxxxxxxxxx",
    "username""Darker"
}

2. 编写1个函数

from homework.serializer import UserReadOnlyModelSerializer
    def jwt_response_payload_handler(token, user=None, request=None):
        return {
            "code":  100,
            "msg": "登录成功",
            "token""xxxxxx.xxxxxxx.xxxxxxxxxxx",
            "username"from jwt_test.serializer import UserReadOnlyModelSerializer
        }

3. 在settings.py中配置

import datetime
JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'jwt_test.utils.jwt_response_payload_handler',
}

十:基于JWT的多方式登录

多方式登录

  • 手机号 + 密码
  • 用户名 + 密码
  • 邮箱 + 密码

登录流程分析(POST请求)

视图类:

  • ViewSet(ViewSetMixin, views.APIView`)可以自动生成路由
  • ViewSet中有login方法

路由:

  • 自动生成

序列化类:

  • 重写validate方法, 在这里对用户名和密码进行校验

代码实现

models.py
form django.contrib.auth.models import AbstractUser

class UserInfo(AbstractUser):
    phone = models.CharField(max_length=32, unique=True)
    
    
# 自定义的表,不继承AbstractUser
class MyUser(models.model):
    username = models.CharField(max_length=16)
    password = models.CharField(max_length=16)
    phone = models.CharField(max_length=11)
    email = models.EmailField(max_length=16)
    
# 抽象出1个基表(不在数据库里生成,abstract=True),只用来继承
class BaseModel(models.model)
    is_delete = 
settings.py
AUTH_USER_MODEL = 'jwt_test.UserInfo'
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'jwt_test.utils.common_exception'
}
urls.py
from rest_framework.routers import SimpleRouter
from jwt_test.views import LoginView

router = SimpleRouter()
router.register('login', LoginView, basename='login')    # basename相当于起了别名

urlpatterns = [
    
]

urlpatterns += router.urls
views.py
import re

from rest_framework.viewsets import ViewSet
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.views import obtain_jwt_token

from jwt_test.serializer import LoginSerializer
from jwt_test.models import MyUser

jwt_payload_hander = api_settings,JWT_PAYLOAD_HANDLER
jwt_encode_hander = api_settings,JWT_ENCODE_HANDLER

class LoginView(ViewSet):
    def post(self, request, *args, **kwargs):
        # 实例化得到1个序列化器的对象
        # ser = LoginSerializer(data=request.data, context={'request': request})
        ser = LoginSerializer(data=request.data)
        # 调用序列化类对象的校验方法
        ser.is_valid(raise_excrption=True)    # 字段自己的校验,全局钩子、局部钩子
        # 如果通过:表示登录成功,返回手动签发的token(返回数据的格式可以自己控制)
        # token = ser.tk    # 不建议用
        token = ser.context.get('token')
        username = ser.context.get('username')
        retuen APIResponse(token=token, username=username)
        # 如果不通过:表示登录失败,可以不管(因为自定义了全局异常)
        
class MyLoginView(APIView):
    def post(self, request, *args, **kwargs):
        username = attrs.get('username')
        password = attrs.get('password')
        if re.match('^1[3-9]\d{9}$', username):
            user = MyUser.objects.filter(phone=username).first()
        elif re.match('^.+@.+$', username):
            user = MyUser.objects.filter(email=username).first()
        else:
            user = MyUser.objects.filter(username=username).first()
        if user and user.password == password:
            payload = jwt_payload_handler(user)    
            token = jwt_encode_handler(payload)
            self.context.['token'] = token
            self.context.['username'] = user.username
            return APIResponse(token=token, username=username)
        else:
            return APIResponse(code=500, msg='用户名或密码错误')
        
class Order(APIView):
    def get(self, request):
        authentication_classes = [JWTAuthentication,]
        # print(request.user)    # 自定义的user对象
        print(request.user)    # user是1个字典,内部有:user_id
        # 后续要查询该用户的所有订单,直接根据user_id查询即可:[] 或者 . 取值
        return APIResponse(msg='订单查询成功!')
serializer.py
import re

from rest_framework import serializers
from rest_framework_jwt.views import obtain_jwt_token
from rest_framework_jwt.utils import jwt_encode_handler
from rest_framework_jwt.utils import jwt_payload_handler

from jwt_test.models import UserInfo



class LoginSerializer(serializers.ModelSerializer):
    # 重写usernane字段
    username = serializers.CharField()
    class Meta:
        model = UserInfo
        fields = ['username', 'password']
        
    def validate(self, attrs):
        # username可能是邮箱、手机号、用户名
        username = attrs.get('username')
        password = attrs.get('password')
        # 如果是手机号
        if re.match('^1[3-9]\d{9}$', username):
            # 以手机号登录
            user = UserInfo.objects.filter(phone=username).first()
        elif re.match('^.+@.+$', username):
            # 以邮箱登录
            user = UserInfo.objects.filter(email=username).first()
        else:
            # 以用户名登录
            user = UserInfo.objects.filter(username=username).first()
        # 如果user有值,并且密码正确
        if user and user.check_password(password):
            # 登录成功,生成token并返回
            # def-jwt中,有1个通过user对象生成token的方法
            payload = jwt_payload_handler(user)    # 通过user拿到payload
            token = jwt_encode_handler(payload)    # 通过payload拿到token
            # token是要在视图类中使用的,现在token是在序列化类中的,要实现通信
            # self.tk = token    # 这里的self就是序列化类的对象,这种方式可以拿 但是不好,建议用下面的方法
            # self.context.get('request')
            # 视图类 和 序列化类 之间,通过context这个字典来传递数据
            self.context.['token'] = token
            self.context.['username'] = user.username
            return attrs
        else:
            raise ValidationError('用户名或密码错误')
        
utils.py
from rest_framework.response import Response
from rest_framework.views import exception_handler

class APIResponse(Response):
    def __init__(self, code=100, msg='成功', data=None, status=None, headers=None, content_type=None, **kwargs):
        dic = {'code': code, 'msg': msg}
        if data:
            dic['data'] = data
            
        dic.update(kwargs)
        super().__init__(data=dic, status=status, headers=headers,content_type=content_type)
        
        
def common_exception(exc, context):
    response = exception_handler(exc, context)
    if response is None:
        response = Response({'code': 666, 'detail': '发生了未知错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

    return response
auth.py
from rest_framework_jwt.authentication import BaseJSONWenTokenAuthentication
from rest_framework_jwt.exceptions import AuthenticationFailed
from rest_framework_jwt.utils import jwt_decode_handler
import jwt

class JwtAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        # 认证逻辑()
        # token信息可以放在请求头中,请求地址中
        # key值可以随意叫
        # token=request.GET.get('token')
        token=request.META.get('HTTP_Authorization'.upper())
        # 校验token是否合法
        try:
            payload = jwt_decode_handler(token)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('过期了')
        except jwt.DecodeError:
            raise AuthenticationFailed('解码错误')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('不合法的token')
        user = payload
        return (user, token)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值