🍖JWT 认证

一.JWT 介绍

  • Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)
  • 该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景
  • JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源
  • 也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密

二.JWT认证与session认证的区别

1.基于session认证流程图

image-20210423192305768

服务器需要存储用户的token信息

2.基于jwt认证流程图

image-20210423193036685

服务端不需要存储用户token, 都存在客户端

三.JWT 的构成

JWT就是一段字符串, 由三段信息构成, 三段信息文本使用.(点) 拼接就构成了JWT字符串 :

  • eyJhbGciOiJIUzI1sNiIsIn.eyJzdWIiOiIxMjRG9OnRydWV9.TJVArHDcEfxjoYZgeFONFh7HgQ
  • 第一部分我们称它为头部 : header
  • 第二部分我们称其为载荷 : payload (类似于飞机上承载的物品)
  • 第三部分是签证 : signature

1.头部 : header

两部分组成 :

  • 声明类型(当前令牌名称)
  • 声明加密算法
{
  'typ': 'JWT',
  'alg': 'HS256'
}

将头部使用base64编码构成第一部分 (下面介绍base64编码方法, 该编码可以对称解码)

eyJ0eXAiOiJKV1iIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiQiLCJhbGciO

2.载荷 : payload

存放用户有效信息的地方, JWT 规定了7个官方字段, 可以选用

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除了上面的字段, 你自己也可以添加自己想要的字段, 需要注意的是:这些信息是不加密的, 所以最好不要存敏感信息

{
  "sub": "1234567890",
  "name": "John Doe",
  "age": 23
  "admin": true
}

将载荷使用base64编码构成第二部分

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

3.签名 : signatrue

signature 由三部分构成 :

  • base64 编码后的 header
  • base64 编码后的 payload
  • secret : 秘钥 (只有服务端知道)
# 使用header中指定的加密算法将三个信息以下面的方式进行加密
string = [base64 编码后的 header] + "." + [base64 编码后的 payload]  # 字符串拼接
HMACSHA256(string, secret)  # 加密算法加密得到加密摘要(TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONF)

4.得到 token

算出签名之后, 把 header、payload、signatrue 三部分使用 .(点) 拼接成一个大字符串, 然后返回给客户端让其存储

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意 :

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

四.base64 编码和解码的使用

首先 base64 是一种编码方式, 并非加密方式; 它跟语言无关, 任何语言都能使用 base64 编码&解码

1.base64 编码

import base64
import json

# 定义一个信息字段
dic = {"id": 1, "name": "shawn", "age": "male"}

# 将其序列化成json格式字符串
json_str = json.dumps(dic)

# 将json格式字符串encode再使用base64编码成一串Bytes格式编码
base64_bytes = base64.b64encode(json_str.encode('utf-8'))

print(base64_bytes)  
# b'eyJpZCI6IDEsICJuYW1lIjogInNoYXduIiwgImFnZSI6ICJtYWxlIn0='
print(base64_bytes.decode('utf-8'))  
# eyJpZCI6IDEsICJuYW1lIjogInNoYXduIiwgImFnZSI6ICJtYWxlIn0=

得到的字符串后面有等号, 代表不是 4 的倍数使用等号填充

2.base64 解码

import base64
import json

bytes_str = b'eyJpZCI6IDEsICJuYW1lIjogInNoYXduIiwgImFnZSI6ICJtYWxlIn0='
res = base64.b64decode(bytes_str)

print(res)  # b'{"id": 1, "name": "shawn", "age": "male"}'

五.JWT 的原理

JWT的本质原理就是签发校验 : 关于签发和核验JWT, 我们可以使用Django REST framework JWT扩展来完成

1.签发

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

  • 用基本信息存储 json 字典, 采用 base64 编码得到头字符串
  • 用关键信息存储 json 字典,采用 base64 编码得到体字符串
  • 用头、体编码的字符串再加安全码信息(secret)存储 json 字典, 采用 header 中指定的算法加密得到签名字符串

2.校验

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

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

六.DRF 项目的 JWT 认证开发流程

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

ps : 登录接口需要做 认证 + 权限 两个局部禁用

七. drf-jwt 的安装和基本使用

JWT的本质原理就是签发校验 : 关于签发和核验JWT, 我们可以使用Django REST framework JWT扩展来完成

1.官网下载

👉GitHub : https://github.com/jpadilla/django-rest-framework-jwt

2.pip 安装

pip install djangorestframework-jwt

3.简单使用

  • 简单使用 : 自动签发 token 和自动认证
# 默认使用的是 auth 的 user 表
# 不需要书写登入功能以及认证类, jwt都内置了
  • 先在 auth_user 中创建一个用户
# 终端
manage.py@jwt_test > createsuperuser
用户名 : ...
邮箱 : 直接回车跳过
密码: ...
确认密码: ...
  • urls.py
from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token  # 就是一个视图函数

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', obtain_jwt_token),  # jwt提供的认证
]
  • Postman 中进行测试

jwtddd

提交数据, jwt 认证进行校验并签发 token, 返回客户端

  • obtain_jwt_token 的本质, 我们可以 Ctrl+点击 进入源码查看

image-20210423212934832

# 所以说它本质就是一个视图类, 我们也可以这样填写👇
path('login/', ObtainJSONWebToken.as_view()),

4.djangorestframework-jwt 的默认配置文件

# djangorestframework-jwt也有配置文件,也有默认配置
# 默认过期时间是 5 分钟 (可以进行配置)

# settings.py 文件
import datatime

JWT_AUTH = {
    # 自定义过期时间1天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    # 也可以自定义认证结果reaponse (下面介绍)
  	# 如果不自定义, 返回的格式是固定的, 只有token字段(如上面演示所看到的)
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.custom_jwt_response_payload_handler',  
}

八.JWT 实现自动签发 token, 自定义响应格式, 限制某接口登入后访问

1.需求

  • 使用 jwt 内置登入认证, 但自定义 response 内的信息
  • 限制查询图书接口必须登录才能用 (加在视图类中)

2.书写自定义响应格式

  • 我们可以先查看默认的 jwt 响应
# 导入 jwt 响应, Ctrl+点击 进入源码查看
from rest_framework_jwt.utils import jwt_response_payload_handler

image-20210423215358111

我们直接 copy 过来修改

  • myresponse.py
# 重写该响应
def custom_jwt_response_payload_handler(token, user=None, request=None):
    return {
        'status': 200,
        'messages': 'success!',
        'user_id': user.id,
        'username': user.username,
        'token': token
    }

3.自定义 djangorestframework-jwt 配置

  • settings.py
import datatime

JWT_AUTH = {
    # 自定义过期时间7天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
    # 指定自己重写的 jwt 响应
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'drf_test.myresponse.custom_jwt_response_payload_handler',
}

4.书写其他文件逻辑

  • models.py
# 创建建书籍表,并自行添加书籍
from django.db import models

class Book(models.Model):
    name = models.CharField(max_length=32)
    price = models.IntegerField()
  • serializer.py
from rest_framework import serializers
from drf_test import models

# 创建一个书籍的序列化类
class BookModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Book
        fields = "__all__"
        extra_kwargs = {
            'title': {
                'help_text': '这是书籍的名字!!'
            }
        }
  • views.py
from drf_test import serializer
from drf_test import models
from rest_framework.viewsets import ModelViewSet
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.pagination import PageNumberPagination
PageNumberPagination.page_size = 2  # 设置分页的单页显示条数


class BookView(ModelViewSet):
    queryset = models.Book.objects.all()
    serializer_class = serializer.BookModelSerializer
    pagination_class = PageNumberPagination

    # 如果使用jwt内置的认证类, 需要配合一个权限类
    # 因为只加认证类, 只要带了token, 就认证, 不带token就不认证
    # 所以就算你不带token也可以查到书籍,这不是我们想要的效果
    authentication_classes = [JSONWebTokenAuthentication, ]
    # 判断当前登录用户是否通过了认证(需要配合这个权限类)
    permission_classes = [IsAuthenticated, ]

    def list(self, request, *args, **kwargs):
        print(request.user)
        return super().list(request, *args, **kwargs)

5.测试效果

  • 自定义响应测试

222222vbnm

  • 测试认证通过后能访问书籍, 不通过无法访问

33333vbnm


4444vbnm

6.头部访问格式

  • 上面测试注意事项 : token 放在 Headers 中, 并且 Key 为 Authorization (源码里规定这么写的)

image-20210423225032533

  • 使用 jwt 内置的认证, 如果没有修改配置文件中配置的前缀, 那么 jwt前缀(大小写都行)必须要加, 如果不加前缀认证就返回 None, 认证就失效了
# 格式
Authorization:JWT [三段式的 token]  # jwt + 空格 + token,源码里是以空格切分

image-20210423225221139

  • 也可以修改认证前缀
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'drf_test.myresponse.custom_jwt_response_payload_handler',
'JWT_AUTH_HEADER_PREFIX': 'MYJWT',  # 了解一下就行, 一般不改
}

九.源码Copy+修改实现自定义认证类

1.jwt 自带认证类源码

  • 上面的实战例子我们使用的是 jwt 自带的认证类, 如果我们要自己手动写, 就必须重写 authenticate 方法, 我们看看其源码的逻辑:

image-20210424151428435

2.自定义认证类

  • 自建一个 auth 认证类文件书写
import jwt
from drf_test import models
from rest_framework import exceptions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.settings import api_settings

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
'''
JWT_DECODE_HANDLER 在源码中所对应的方法:'rest_framework_jwt.utils.jwt_decode_handler'
将该方法内存地址赋值给:jwt_decode_handler
'''

# 继承JSONWebTokenAuthentication类来书写
class CustomJsonAuthentication(JSONWebTokenAuthentication):
    # 认证类必须重写 authenticate 方法
    def authenticate(self, request):
        # 1.获取到request中的token
        jwt_value = self.get_jwt_value(request)
        # 2.也可以直接将token放在url中使用get来获取(但一般不这么做)
        # jwt_value = self.request.GET.get('token')
        # 3.还可以从META中获取
        # jwt_value = request.META.get('HTTP_AUTHORIZATION')
        if jwt_value:
            # 验证签名
            try:
                # 得到荷载
                payload = jwt_decode_handler(jwt_value)
                # 直接从数据库中找出用户对象(每次都要查询数据库,消耗大,并且并不是所有字段都需要)
                # user_obj = models.User.objects.filter(id=payload['user_id']).first()
                # 可以临时生成一个只有id和username的对象,只有需要的时候才拿出id或username去数据库中过滤用户
                user_obj = models.User(id=payload['user_id'],username=payload['username'])
                # 又或者直接使用字典的方式(能取出值就ok)
                # user_obj = {'id':payload['user_id'],'username':payload['username']}

            except jwt.ExpiredSignature:
                raise exceptions.AuthenticationFailed('token已过期')
            except jwt.DecodeError:
                raise exceptions.AuthenticationFailed('签名错误')
            except jwt.InvalidTokenError as e:
                raise exceptions.AuthenticationFailed(e)
            return user_obj,jwt_value
        raise exceptions.AuthenticationFailed('请携带token进行认证!')

3.在视图中使用自定义的认证类

  • views.py
from drf_test import serializer
from drf_test import models
from rest_framework.viewsets import ModelViewSet
from rest_framework.pagination import PageNumberPagination
PageNumberPagination.page_size = 2

# 导入自定义的认证类
from drf_test.auth.customauth import CustomJsonAuthentication

class BookView(ModelViewSet):
    queryset = models.Book.objects.all()
    serializer_class = serializer.BookModelSerializer
    pagination_class = PageNumberPagination

    # 自己写的认证类
    authentication_classes = [CustomJsonAuthentication,]

    def list(self, request, *args, **kwargs):
        print(request.user)
        return super().list(request, *args, **kwargs)

4.测试效果

  • 不携带 token

dddddsssss4444

  • 提供错误的 token

dddddsssss444222224

  • 验证通过

dddddsssss4433333333333

十.自定义登入并签发 token

1.签发 token 源码分析

  • 如果我们要实现自定义的 token 签发就必须了解 jwt token签发的原理

  • 前面我们自定义响应格式的时候返回了一个 token,这里已经有 token,那么 token 是在哪里产生的呢?

  • 我们猜想应该是request传进来到序列化类进行校验, 校验成功后就签发 token 的

  • 查看 jwt 的序列化类

image-20210424160148859

  • jwt_payload_handler 源码分析
# 导入该方法 Ctrl + 点击 进入
from rest_framework_jwt.utils import jwt_payload_handler

  • jwt_encode_handler 源码分析

image-20210424163113104

image-20210424163445963

2.自定义签发 token

  • 将 auth_user 表该个名, 自己新建个 User 表方便实验 (models.py)
from django.db import models
from django.contrib.auth.models import AbstractUser


class UserInfo(AbstractUser):
    phone = models.BigIntegerField(null=True)


class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)

存入用户名和密码

  • views.py
from rest_framework.response import Response
from rest_framework.views import APIView
# 导入 jwt 配置文件
from rest_framework_jwt.settings import api_settings

# 将获取 payload 的方法以及签发 token 的方法内存地址赋值给两个变量
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


class UserLoginView(APIView):
    def post(self, request, *args, **kwargs):
        username = request.data.get('username')
        password = request.data.get('password')

        user = models.User.objects.filter(username=username, password=password).first()
        if user:
            # 调用两个方法签发 token
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            return Response({'status': 200, 'msg': '登入成功!', 'token': token})
        else:
            return Response({'status': 201, 'msg': '用户名或密码错误!'})
  • urls.py
path('login2/', views.UserLoginView.as_view()),

3.测试效果

  • 用户名或密码错误测试

login222223

  • 登入成功测试

login2333333

十一.实现多方式登入, 并将逻辑写在序列化类中

1.需求

  • 登入方式多种 : (用户名、输入手机号、 输入邮箱) + 密码
  • 逻辑书写在序列化类中

2.代码实现

  • serializer.py
from django.db.models import Q  # 用来构建或与非
from rest_framework.exceptions import ValidationError
# 导入 jwt 配置文件
from rest_framework_jwt.settings import api_settings
# 将获取 payload 的方法以及签发 token 的方法内存地址赋值给两个变量
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER


# 多登入方式序列化类
class VariousLoginModelSerializer(serializers.ModelSerializer):
    # 重写 username 字段让其时区字段自己的校验规则, 不然会出现required错误
    username = serializers.CharField()

    class Meta:
        model = models.UserInfo
        fields = ['username', 'password']

    def validate(self, attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        # 多登入方式,username可能是用户名、邮箱、手机号,分情况操作
        if username.isdigit():
            user = models.UserInfo.objects.filter(Q(phone=int(username))).first()
        else:
            user = models.UserInfo.objects.filter(Q(email=username) | Q(username=username)).first()

        if user and user.check_password(password):
            # 登入成功,签发token
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            # context是上下文,是视图类和序列化类沟通的桥梁(管道)
            self.context['token'] = token
            self.context['username'] = username
            self.context['user_obj'] = user  # 将当前user对象也放进去
            return attrs
        else:
            raise ValidationError('用户名或密码错误!!')
  • views.py
from rest_framework.generics import CreateAPIView
from drf_test import models
from drf_test import serializer
from rest_framework.response import Response
from rest_framework.viewsets import ViewSetMixin

# 多种登入方式(继承ViewSetMixin自动生成路由)
class VariousLoginView(ViewSetMixin, CreateAPIView):
    queryset = models.UserInfo.objects.all()
    serializer_class = serializer.VariousLoginModelSerializer

    def create(self, request, *args, **kwargs):
        ser = self.get_serializer(data=request.data)
        if ser.is_valid():
            # 从 context 中取出序列化类传过来的数据
            token = ser.context['token']
            username = ser.context['username']
            user_obj = ser.context['user_obj']  # 拿到user对象
            return Response({'status': 200, 'msg': '登入成功!!', 'token': token, 'username': user_obj.username})
        else:
            return Response({'status': 100, 'msg': ser.errors})

3.效果演示

  • 用户名登入

various1111

  • 邮箱登入

various2222

  • 手机号登入

various3333

4.使用正则来匹配是哪种登入方式

  • 只需要改写序列化类中的用户查询的代码
class VariousLoginModelSerializer(serializers.ModelSerializer):
    # 重写 username 字段让其时区字段自己的校验规则, 不然会出现required错误
    username = serializers.CharField()

    class Meta:
        model = models.UserInfo
        fields = ['username', 'password']

    def validate(self, attrs):
        username = attrs.get('username')
        password = attrs.get('password')

        # 多登入方式,username可能是用户名、邮箱、手机号,分情况操作
        if re.match('^1[3-9][0-9]{9}$', username):
            # 用手机号登录
            user = models.UserInfo.objects.filter(phone=username).first()
        elif re.match(r'^.+@.+$', username):  # 这里匹配不严谨(仅用于测试)
            # 以邮箱登录
            user = models.UserInfo.objects.filter(email=username).first()
        else:
            # 以用户名登录
            user = models.UserInfo.objects.filter(username=username).first()

        if user and user.check_password(password):
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            self.context['token'] = token
            self.context['username'] = username
            self.context['user_obj'] = user
            return attrs
        else:
            raise ValidationError('用户名或密码错误!!')
posted @ 2021-04-24 20:35  给你骨质唱疏松  阅读(271)  评论(0编辑  收藏  举报