Published in:2024-10-24 |

drf-day6

1. 请求安全认证

传统安全认证

1604367533343

前后端分离

1604368544777

Token认证方式

1604369711968

1
2
3
4
# 服务端验证的优点:
1. 服务端不需要储存token,token交给每一个客户端储存,服务器压力小
2. 服务器做的是 签发token与校验token的两段算法,签发认证的效率高
3. 算法在集群同步的成本低

2. JWT(Json web token)

github文档:https://github.com/jpadilla/django-rest-framework-jwt

JWT构成

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串

例如:abc.admin.123:头部.载荷.签名

1
2
3
4
5
6
7
8
9
10
11
# jwt构成介绍
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

# jwt每一部分都是由一个json字典加密后形成的字符串

# 头部和载荷采用的是base64可逆加密(前后台都可以解密)

# 签名采用的是sha256不可逆加密

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
1
2
3
4
5
6
{
'typ': 'JWT',
'alg': 'HS256'
}
# 然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
  • payload

载荷就是存放有效信息的地方。 可以保存用户信息,方便在服务端进行反解析

  • signature
  • header (base64后的)
  • payload (base64后的)
  • secret

由以上三个字段串拼接后再使用不可逆的加密算法sha256进行加密

1
# 注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

JWT的使用

pip install djangorestframework-jwt

源码剖析

  • token签发的入口

JSONWebTokenAPIView中的post方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 签发的token的视图请求
# 接受前端的登陆请求并解析参数来调用对应的序列化器
def post(self, request, *args, **kwargs):
# 将前端传递的数据交给序列化器来完成数据的反序列化
serializer = self.get_serializer(data=request.data)

if serializer.is_valid():
# 从序列列化器中解析出当前用户信息与token
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
# Response初始化 将token响应回去
response = Response(response_data)
if api_settings.JWT_AUTH_COOKIE:
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expires=expiration,
httponly=True)
return response

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  • serializer钩子函数

JSONWebTokenAPIView并没有实现token签发的逻辑,而是在子类ObtainJSONWebToken的序列化器中的全局钩子中来处理了token的签发

JSONWebTokenSerializer中的validate函数完成了token的签发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# token签发的核心逻辑
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}

if all(credentials.values()):
# 对当前用户进行认证 返回一个user
user = authenticate(**credentials)

if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)

# 满足所有条件的用户 根据用户的信息生成载荷
payload = jwt_payload_handler(user)

return {
# 根据载荷生成对应的token
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
  • 生成token的方式
1
2
3
4
5
6
7
8
9
10
11
1. 通过username password获取到用户对象,对user进行校验

2. 通过user对象来生成载荷:`jwt_payload_handler(user)`
'rest_framework_jwt.utils.jwt_payload_handler',

3. 通过载荷payload来签发token: `jwt_encode_handler(payload)`
'rest_framework_jwt.utils.jwt_payload_handler',

# 总结:
1. 签发token走的是`ObtainJSONWebToken`父类中的post方法
2. user 与 token是在ObtainJSONWebToken自己的序列化器中的全局钩子中来完成获取的

3. 多方式登陆 签发token

签发token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import re

from rest_framework.serializers import ModelSerializer
from rest_framework import serializers
from rest_framework_jwt.settings import api_settings

from api.models import User

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserModelSerializer(ModelSerializer):
"""
前端发送请求 传递参数 但数据不需要保存至数据
反序列化的过程中,有些字段只参与反序列化的业务,并不会保存到数据中,模型也没有对应的字段
"""
# 自定义反序列化字段 代表这个字段只参与反序列化 且不会要求这个字段与model类进行映射
account = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True)

class Meta:
model = User
fields = ["account", "password", "username", "phone", "email"]

extra_kwargs = {
"username": {
"read_only": True
},
"phone": {
"read_only": True
},
"email": {
"read_only": True
},
}

def validate(self, attrs):
account = attrs.get("account")
password = attrs.get("password")

# 对各种登陆方式做处理 账号 邮箱 手机号
if re.match(r'1[3-9][0-9]{9}', account):
user_obj = User.objects.filter(phone=account).first()
elif re.match(r'.+@.+', account):
user_obj = User.objects.filter(email=account).first()
else:
user_obj = User.objects.filter(username=account).first()

# 判断用户是否存在
if user_obj and user_obj.check_password(password):
# 签发token
"""
根据用户信息生成载荷
根据载荷生成token
"""
payload = jwt_payload_handler(user_obj)
token = jwt_encode_handler(payload)
self.obj = user_obj
self.token = token

return attrs

自定义校验token

继承BaseJSONWebTokenAuthentication,重写authenticate方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import jwt
from rest_framework import exceptions
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication, jwt_decode_handler


class JWTAuthentication(BaseJSONWebTokenAuthentication):

def authenticate(self, request):
# 获取前端传递的token
jwt_token = request.META.get("HTTP_AUTHORIZATION", None)
print(jwt_token)
# 自定义对于token的校验规则
token = self.parse_jwt_token(jwt_token)

if token is None:
return None

# 将通过token反解析出载荷
try:
payload = jwt_decode_handler(token)
except jwt.ExpiredSignature:
raise exceptions.AuthenticationFailed("签名已过期")
except:
raise exceptions.AuthenticationFailed('非法用户')

# 如果没有任何错误 则代表解析通过
user = self.authenticate_credentials(payload)

return user, token

def parse_jwt_token(self, jwt_token):
tokens = jwt_token.split()

if len(tokens) != 3 or tokens[0].lower() != "auth" or tokens[2].lower() != "jwt":
return None

return tokens[1]

4. 搜索与排序组件

1
2
3
4
5
6
7
8
9
10
class ComputerListAPIView(ListAPIView):
queryset = Computer.objects.all()
serializer_class = ComputerModelSerializer

# 通过filter_backends来配置你要使用的过滤器类
filter_backends = [SearchFilter, OrderingFilter]
# 指定你要搜索的字段|条件
search_fields = ['name', "price"]
# 指定排序的条件
ordering = ['price']

5. 分页组件

基础分页器

1
2
3
4
5
6
7
8
9
class MyPageNumberPagination(PageNumberPagination):
# 指定每页分页的数量
page_size = 2
# 指定每页分页的最大数量
max_page_size = 5
# 指定前端修改每页分页数量的key
page_size_query_param = "page_size"
# 手机指定获取第几页的对象
page_query_param = "page"

偏移分页器

1
2
3
4
5
6
7
8
9
class MyLimitOffsetPagination(LimitOffsetPagination):
# 默认获取每页的数量
default_limit = 3
# 指定前端获取每页数量的key
limit_query_param = "limit"
# 指定偏移开始的位置
offset_query_param = "offset"
# 指定每页获取的最大数量
max_limit = 5

游标分页器(了解)

1
2
3
4
5
6
class MyCursorPagination(CursorPagination):
cursor_query_param = "course"
page_size = 3
page_size_query_param = "page_size"
max_page_size = 5
ordering = "-price"

django-filter

pip install django-filter==2.2.0

  • filter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
from django_filters.rest_framework import FilterSet

from api.models import Computer


class ComputerFilterSet(FilterSet):
from django_filters import filters
min_price = filters.NumberFilter(field_name="price", lookup_expr="gte")
max_price = filters.NumberFilter(field_name="price", lookup_expr="lte")

class Meta:
model = Computer
fields = ["brand", "min_price", "max_price"]
  • views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ComputerListAPIView(ListAPIView):
queryset = Computer.objects.all()
serializer_class = ComputerModelSerializer

# 通过filter_backends来配置你要使用的过滤器类
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
# 指定你要搜索的字段|条件
search_fields = ['name', "price"]
# 指定排序的条件
ordering = ['price']

# 查询价格大于 3000 小于8000的电脑
# django-filter查询 通过filter_class 指定过滤器
filter_class = ComputerFilterSet

作业

1
2
3
1. 掌握JWT签发与校验token的方式,多方式签发token
2. 掌握过滤组件以及django-filter插件的使用
3. 总结知识图谱
Prev:
Next: