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部分信息(声明):
- 声明类型:这里用的是
JWT
- 声明加密的算法:通常直接使用
HMAC SHA256
- 除此之外,也可以添加其他声明:比如说,添加公司名称等信息
自定义header
,JSON
格式:
{
"type": "JWT",
"encode_method": "HASH256"
}
用base64
对其进行编码,得到JWT
中的header
部分:
ewogICAgInR5cGUiOiJKV1QiLAogICAgImVuY29kZV9tZXRob2QiOiJIQVNIMjU2Igp9
payload
荷载部作为JWT
三部分中的第二部分,都是存放有效信息。
它可以存放三种类型的有效信息:
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明(建议但不强制使用):
荷载部位的key | 描述 |
---|---|
iss | JWT 签发者(服务端) |
sub | JWT 所面向的用户 |
aud | 接收JWT 的一方 |
exp | JWT 的过期时间,该时间必须大于签发时间 |
nbf | 再某一时间段之前,该JWT 不可用 |
jti | JWT 的唯一身份标识,主要用作一次性token ,回避时序攻击 |
公共的声明:
- 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但
不建议添加敏感信息
,因为该部分在客户端可解密
。
私有的声明:
- 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为
base64
是对称解密的,意味着该部分信息可以归类为明文信息
。
自定义payload
,JSON
格式:
{
"id": 1024,
"sub": "1233211234567",
"name": "Darker",
"admin": true
}
用base64
对其进行编码,得到JWT
中的payload
部分:
ewogICAgImlkIjogMTAyNCwKICAgICJzdWIiOiAiMTIzMzIxMTIzNDU2NyIsCiAgICAibmFtZSI6ICJEYXJrZXIiLAogICAgImFkbWluIjogInRydWUiCn0=
signature
JWT
的第三部分是一个签证信息,这个签证信息由三部分组成
header
(base64
后的)payload
(base64
后的)secret
- 这个部分需要
base64
加密后的header
和base64
加密后的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认证算法:签发与校验
-
JWT分成3部分:头(
header
)、体(payload
)、签名(signature
) -
头(
header
)和体(payload
)是可逆加密,让服务器可以反解出user
对象;签名(signature
)是不可逆加密,保证整个token
的安全性的 -
头体签名三部分,都是采用
json
格式的字符串,进行加密,可逆加密一般采用base64
算法,不可逆加密一般采用hash(md5)
算法 -
头(
header
)中的内容是基本信息:公司信息、项目组信息、token
采用的加密方式信息{ "company": "公司信息", "type": "JWT", "encode_method": "HASH256" ... }
- 体(
payload
)中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间{ "id": 1024, "sub": "1233211234567", "name": "Darker", "admin": true }
- 签名中的内容时安全信息:
头的加密结果
+体的加密结果
+服务器不对外公开的安全码
进行md5
加密{ "head": "头的加密字符串", "payload": "体的加密字符串", "secret_key": "安全码" }
签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token
- 用基本信息存储
json
字典,采用base64
算法加密得到头字符串
- 用关键信息存储
json
字典,采用base64
算法加密得到体字符串
- 用头、体加密字符串再加安全码信息存储
json
字典,采用hash md5
算法加密得到签名字符串
- 账号密码就能根据
User
表得到user
对象,形成的三段字符串 用.
拼接成token
返回给前台
校验:根据客户端带token的请求 反解出 user 对象
- 将
token
按.
拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理- 第二段 体加密字符串,要反解出
用户主键
,通过主键从User
表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token
没过期,且是同一设备来的- 再用
第一段
+第二段
+服务器安全码
不可逆md5
加密,与第三段签名字符串
进行碰撞校验,通过后才能代表第二段校验得到的user
对象就是合法的登录用户
drf项目的jwt认证开发流程(重点)
- 用账号密码访问登录接口,登录接口逻辑中调用
签发token 算法
,得到token
,返回给客户端
,客户端自己存到cookies
中校验token的算法
应该写在认证类中(在认证类中调用),全局配置
给认证组件,所有视图类请求
,都会进行认证校验
,所以请求带了token
,就会反解出user
对象,在视图类中用request.user
就能访问登录的用户
- 注:登录接口需要做
认证
+权限
两个局部禁用
四:制作JWT
Python进行演示,如何创建一个JWT:
- 进行
header
头部声明- 进行
payload
荷载声明,不要存放密码等敏感信息,因为可通过反解出来(也可以存放过期时间等)- 将头部与荷载进行.拼接
- 将头部与荷载的信息与盐进行
hash256
加密(通常加密方式都是在alg
中声明的),得到签证/签名- 通过.拼接出
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
中的header
和payload
-> 用一样的加密方式和密码得到一个signature
-> 和该Token
的signature
进行比较
JWT的验证流程
原生token验证的流程:
六:JWT的优势
如果要体现JWT的优势,则需要与cookie
以及session
做对比。
-
cookie
的劣势:
主要是存储时不安全,所有数据存放只用户本地,一旦被窃取就可以伪造登录。 -
session
的劣势主要有三点:- 数据存放至服务器,占用服务器资源。
- 每次用户登录成功后,都需要向数据库中写入
session
,速度缓慢。 - 对于集群式的部署,如果存储
session
的数据库不一致,则会是个大麻烦,因为用户如果接入了不同的服务器,则意味着写入的session
也在不同的数据库中。这会导致用户的session
在不同数据库中会存在多次写入的问题。
-
了解了cookie以及session的劣势后,jwt的优势就显而易见。
- 用户数据存放至本地,但必须要与服务端存储的盐进行对比一致后才认证成功。
- 不需要有数据库写入的操作。
- 集群式部署时也没有任何问题,前提是每个服务器的盐都一样。
七: JWT的安装与使用
安装
- 官网
http://getblimp.github.io/django-rest-framework-jwt/ - 用
pip
安装pip install djangorestframework-jwt
快速使用(默认使用auth
的user
表)
- 在默认
auth
的user
表中创建一个用户 - 在路由中配置
from rest_framework_jwt.views import obtain_jwt_token urlpatterns = [ path('login/', obtain_jwt_token), ... ]
- 用
postman
向这个地址发送post
请求,携带用户名、密码,登陆成功就会返回token
obtain_jwt_token
本质也是一个视图类
,继承了APIView
- 通过前端传入的用户名密码,校验用户,如果校验通过,生成
token
,返回 - 如果校验失败,返回错误信息
- 通过前端传入的用户名密码,校验用户,如果校验通过,生成
- 如果用户携带了
token
,并且配置了JSONWebTokenAuthentication
,从request.user
就能拿到当前登录用户,如果没有携带,当前登录用户就是匿名用户 - 前端要发送请求,携带
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)