签发token

一. 前戏

cookie,session,token

    -cookie是存放在客户端浏览器的键值对


    -session是存放在服务端的键值对
        客户端:sessionId:asdfasdf----> 放到cookie中
        服务端:
            asdfasdf:{id:3,name:lqz}
            dddddd:{id:4,name:pyy}


    -token: 字符串:分三段
        -第一段:头-->      公司信息,加密方式。。。
        -第二段:荷载-->    放用户信息---->{id:3,name:lqz}
        -第三段:签名--->   把第一段和第二段通过某种加密方式+秘钥加密得到一个字符串 asdfads
        -把三段使用base64编码后拼到一起 -----类似-----> dasfasd.asdfasdasd.asdfads 这种

        -【token 的签发】--->    登录成功后,服务端生成
        - 把token串给前端--->    前端拿着
        -后面前端只要发请求,就携带token串到后端
        -【token的验证】--》拿到第一段和第二段使用同样的加密方式+秘钥再加密--->   得到字符串跟第三段比较,如果一样,表示没有被篡改,如果不一样,表明被篡改了,token不能用了
        -如果没有被篡改,取出第二段  当前登录用户的信息  id

二. jwt组成

base64编码样子,可以用 base64 进行解码查看。

# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjgxMjA4MzE2LCJlbWFpbCI6IjEyM0BxcS5jb20ifQ.ZrQRrw5yolp40cWsDAU2JirrKhKQLydZH-hbvCYzYuM

# 第一段:头部:header
    # 声明加密算法,公司信息。。。
    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

# 第二段:荷载:payload
    # 用户id,用户信息,token过期时间 exp,token签发时间 iat
    eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjgxMjA4MzE2LCJlbWFpbCI6IjEyM0BxcS5jb20ifQ


# 第三段:签名 signature
    # 通过某种加密方式+秘钥,把头和荷载加密后得到
    # 使用它,做到防篡改,防伪造
    ZrQRrw5yolp40cWsDAU2JirrKhKQLydZH-hbvCYzYuM

三. 旧的 JWT

1. 使用导航

# 1. 下载
pip3 install djangorestframework-jwt


# 2. 路由层配置
"""必须是auth的user表中有用户, 自己创的表不行"""
"""通过 createsuperuser 创建用户"""
## 导入
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    ## 注册使用
    path('login/', obtain_jwt_token),
]

# 3. 视图类配置
class CarView(ViewSet):
    # 认证类《必须配和下面的权限类一起使用》
    authentication_classes = [JSONWebTokenAuthentication,]
    # 权限类
    permission_classes = [IsAuthenticated,]

# 4. 访问

## (1)发送post请求
http://127.0.0.1:8009/login/

## (2)携带参数 《 json 类型 》
{
    # key 必须是 username, password
    "username": "admin",
    "password": "123456"
}

## (3)携带参数 《 请求头中 》,key 必须是 Authorization
{key: value} ===> {Authorization : jwt token串}

# token 串样式
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjgxMjA4MzE2LCJlbWFpbCI6IjEyM0BxcS5jb20ifQ.ZrQRrw5yolp40cWsDAU2JirrKhKQLydZH-hbvCYzYuM

2. 修改还回格式

配合标题 (1) 中的《使用导航》接着写

(1). 函数

新建一个py文件, 写一个函数

def jwt_response_payload_handler(token, user=None, request=None):
    return {'code': 100, 'msg': '登陆成功', 'token': token, 'username': user.username}

(2). 配置

在配置文件中配置

JWT_AUTH = {
    # value值是函数的位置
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
}

3. 自定义

(1). 登陆时签发 token

可以自己定义用户表,实现签发token功能

from rest_framework_jwt.settings import api_settings
from rest_framework.decorators import action
from .models import User


class UserView(ViewSetMixin, APIView):
    @action(methods=['POST', 'GET'], detail=False)
    def login(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        print('username', username, 'password', password)          # {'pyy':'123'}
        user = User.objects.filter(username=username, password=password).first()

        if user:
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(user)                    # 根据当前登录用户获取荷载
            print(payload)                                         # {'user_id': 1, 'username': 'pyy', 'exp': datetime.datetime(2023, 4, 11, 11, 25, 37, 297881)}
            token = jwt_encode_handler(payload)                    # 根据荷载生成token
            return Response({'code': 100, 'msg': '登录成功', 'token': token, 'user': username})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误'})

(2). 认证类(验证token)

新建一个py文件,编写认证类,重写authenticate方法

import jwt
from rest_framework import exceptions
from rest_framework_jwt.settings import api_settings
from rest_framework.authentication import BaseAuthentication

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
from .models import User
from rest_framework.response import Response


class TokenAuthentication(BaseAuthentication):
    def authenticate(self, request):

        # 这里获取不一定非要在请求头中(可以放在别的地方《但必须指明接口文档》)
        # jwt_value = request.query_params.get('token') or request.data.get('token')
        jwt_value = request.META.get('HTTP_TOKEN')                   # 1 取出 token

        """
            eg: 请求头传递数据 header
                参数名:token,  参数值:"cdsvsdvmkdvkk"

            取值方式:  request.META.get('HTTP_TOKEN')
                      会转成 HTTP_ 加上 参数名(大写) ===> HTTP_TOKEN
                      Dbug 模式貌似查看不到
        """

        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            msg = 'token过期了'
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = 'token解码错误或为None'
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            msg = '解析token未知错误'
            raise exceptions.AuthenticationFailed(msg)
        print(payload)                                              # 荷载---> user_id
        user = User.objects.filter(pk=payload['user_id']).first()
        return user, jwt_value

        # 效率问题 可以 return user_id, jwt_value  防止接口访问量过大时,因上一步的查询, 造成数据库的负载过重

(3). 注册使用

在别的视图类中使用

class CarView(ViewSet):
    # 注册使用
    authentication_classes = [TokenAuthentication, ]
    # 下面还要配和一个权限类。
    permission_classes = [IsAuthenticated,]
    ......
"""准备"""
# 新建项目没问题,如果是老项目,迁移过数据了,按照如下操作
    1 删数据库
    2 删除项目中app的迁移文件
    3 删除源码中 admin和auth中得迁移记录
    4 扩写auth的user表
    5 重新迁移

四. 新的 JWT

1. 使用导航

# 1. 安装

    pip install djangorestframework-simplejwt


# 2. setting.py 注册

    # (1). 注册 《和 django-rest-framework 一样,属于app》
    INSTALLED_APPS = [
        ...
        'rest_framework_simplejwt',
        ...
    ]

    # (2). 配置过期时间
    import datetime
    SIMPLE_JWT = {
        # ACCESS token有效时长 《用来发请求获取数据》
        'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=30),
        # REFRESH token有效时长 《用来刷新 access》
        'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),
    }


# 3. 迁移表,创建用户 《自己扩写的表也行》
    createsuperuser


# 4. 使用

    # (1). 路由层
    from rest_framework_simplejwt.views import token_obtain_pair, token_verify, token_refresh

    urlpatterns = [
        path('login/', token_obtain_pair),
        path('verify/', token_verify),
        path('refresh/', token_refresh),
    ]

    # (2). 局部或全局注册  *** 必须配合权限类 ***
        ## <1>. 局部注册
        class BookView(APIView):
            authentication_classes = [JWTAuthentication]
            permission_classes = [IsAuthenticated]

        ## <2>. 全局注册
        REST_FRAMEWORK = {
            'DEFAULT_AUTHENTICATION_CLASSES': [
                'rest_framework_simplejwt.authentication.JWTAuthentication',
            ],
            'DEFAULT_PERMISSION_CLASSES': [
                'rest_framework.permissions.IsAuthenticated',
            ],
        }

5. 获取双 token 时,《post请求》
    {
      "username": "",
      "password": ""
    }


6. 前端访问携带 token,《 发请求获取数据时 》
    """*** 式必须如下,放在请求头中 ***"""
    # Authorization 作为 key
    # Bearer + 空格 + access的token
    # 如下:
    Authorization :Bearer access的token


7. 样式:《 双 token 》
{
    # 用来发请求刷新新的 access
    "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcxMjkwNjA1MCwiaWF0IjoxNzEyODE5NjUwLCJqdGkiOiIyODIxYzg3Y2FjZGM0NTZkOGJkZTJmNDY3ZWMxMjRlNiIsInVzZXJfaWQiOjF9.ThssxrQuZobwWRQId_toG_d1ZXMa5QPiM_DoGzqsI7c",

    # 用来发请求获取数据
    "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzEyODIxNDUwLCJpYXQiOjE3MTI4MTk2NTAsImp0aSI6IjZiMzA3ODQwMzBmZjQ2OGI4YzNkNThmYjk0YTJmMDZhIiwidXNlcl9pZCI6MX0.01UXhWe9DOfGh2PDi-WCG9BzvPcQUHYDZostE9-INf4"
}

2. 源码解析

1 中的代码好像只加了个 “路由” 和 “简单的配置” 就能进行一系列的操作。

好像有点蒙, 下面做个简单的解释

# 代码 1

    urlpatterns = [
        # ctrl + 左键 点击 token_obtain_pair,见代码 2
        path('login/', token_obtain_pair),
        path('verify/', token_verify),
        path('refresh/', token_refresh),
    ]

"""-------------以下是 djangorestframework-simplejwt 的源码---------------"""

# 代码 2
    # 实际上是执行了 TokenObtainPairView 这个视图类
    # ctrl + 左键 点击 TokenObtainPairView,见代码 3
    token_obtain_pair = TokenObtainPairView.as_view()

# 代码 3

    class TokenRefreshView(TokenViewBase):

        """里面就写了下面一行代码"""
        # ctrl + 左键 点击 父类 TokenViewBase,见代码 4

        # 这里就是源码自己的 序列化类
        _serializer_class = api_settings.TOKEN_REFRESH_SERIALIZER

# 代码 4
    class TokenViewBase(generics.GenericAPIView):

        # 部分代码省略
        ......

        # 可以看到有个 post 方法, 返回的数据就是我们需要的数据
        def post(self, request: Request, *args, **kwargs) -> Response:
            serializer = self.get_serializer(data=request.data)

            try:
                serializer.is_valid(raise_exception=True)
            except TokenError as e:
                raise InvalidToken(e.args[0])

            # serializer.validated_data 就是序列化校验后的数据, 见代码 5
            return Response(serializer.validated_data, status=status.HTTP_200_OK)


# 代码 5 《 在源码中可以找到, 上述用的就是这个 “序列化类” 》
    # 序列化类
    class TokenObtainPairSerializer(TokenObtainSerializer):
        token_class = RefreshToken

        def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]:
            data = super().validate(attrs)

            # 签发 token 见代码 6
            refresh = self.get_token(self.user)

            # 将 refresh(token)access(token) 放到 data 中,以便后续使用。
            data["refresh"] = str(refresh)
            data["access"] = str(refresh.access_token)

         if api_settings.UPDATE_LAST_LOGIN:
                update_last_login(None, self.user)

            return data


# 代码 6
@classmethod
def get_token(cls, user):
    # for_user(user)真正签发 token, 见代码 7
    return cls.token_class.for_user(user)

# 代码 7
@classmethod
def for_user(cls, user):
    """
    Returns an authorization token for the given user that will be provided
    after authenticating the user's credentials.

    返回将提供的给定用户的授权令牌在验证了用户的凭据之后。
    """
    user_id = getattr(user, api_settings.USER_ID_FIELD)
    if not isinstance(user_id, int):
        user_id = str(user_id)

    token = cls()
    token[api_settings.USER_ID_CLAIM] = user_id

    if api_settings.CHECK_REVOKE_TOKEN:
        token[api_settings.REVOKE_TOKEN_CLAIM] = get_md5_hash_password(
            user.password
        )

    return token

3. 定制还回格式

# 1. 重写 TokenObtainPairSerializer 中 validate 方法,返回什么,前端看到什么

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):

    # 往荷载中加东西
    @classmethod
    # user就是登录成功,查到的用户
    def get_token(cls, user):

        # 签发token
        token = super().get_token(user)
        # 往荷载中加用户名
        token['name'] = user.username

        return token


    def validate(self, attrs):
        old_data = super().validate(attrs)
        data = {
                'code': 200,
                'msg': '登录成功!!!',
                'username': self.user.username,
                'refresh': old_data['refresh'],
                'access': old_data['access']
                }
        return data


2. 配置文件配置
    SIMPLE_JWT = {
        "TOKEN_OBTAIN_SERIALIZER": "app01.serializer.MyTokenObtainPairSerializer",
    }

4. 多方式登录

** 手机号,用户名 或 邮箱进行登录。**

# 下面三种的 “路由层”
urlpatterns = [
    path('mylogin/', views.UserView.as_view()),
]

(1). 简易版

“视图类” 中编写

class UserView(APIView):
    authentication_classes = ()
    permission_classes = ()

    def post(self, request):

        # 1  request取出用户名和密码
        username = request.data.get('username')
        password = request.data.get('password')

        # 2  使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^.+@.+$', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()

        # 3 校验密码
        if user and user.check_password(password):

            # 4 签发token
            refresh = TokenObtainPairSerializer.get_token(user)

            # 5 返回给前端
            return Response({'code': 100, 'msg': '成功', 'access': str(refresh.access_token), 'refresh': str(refresh)})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误'})

(2). 封装版

“视图类” 配和 “序列化类”


"""-------------------视图类--------------------"""

from rest_framework.generics import GenericAPIView
from .serializer import LoginSerializer

class UserView(GenericAPIView):
    authentication_classes = ()
    permission_classes = ()
    serializer_class = LoginSerializer

    def post(self, request):
        ser = self.get_serializer(data=request.data)

        # 会执行字段自己的校验(没有),执行局部钩子(没有),执行全局钩子(写了:校验用户,签发token)
        if ser.is_valid():

            # context 是视图类和序列化列之间沟通的桥梁
            access = ser.context.get('access')
            refresh = ser.context.get('refresh')
            username = ser.context.get('username')
            return Response({'code': 100, 'msg': '成功', 'username': username, 'access': access, 'refresh': refresh})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误11'})

"""-------------------序列化类--------------------"""

from rest_framework import serializers
from .models import User
import re
from rest_framework.exceptions import ValidationError

class LoginSerializer(serializers.Serializer):

    username = serializers.CharField()
    password = serializers.CharField()

    # 写全局钩子
    def validate(self, attrs):
        # 校验用户,签发token
        username = attrs.get('username')
        password = attrs.get('password')
        # 2  使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^.+@.+$', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            # 3 校验密码
            # 4 签发token
            refresh = TokenObtainPairSerializer.get_token(user)
            self.context['access'] = str(refresh.access_token)
            self.context['refresh'] = str(refresh)
            self.context['username'] = user.username
            return attrs
        else:
            raise ValidationError('用户名或密码错误')

(2). 最终版


"""-------------------视图类--------------------"""

class UserView(GenericAPIView):
    authentication_classes = ()
    permission_classes = ()
    serializer_class = LoginSerializer

    def post(self, request):
        ser = LoginSerializer(data=request.data)
        if ser.is_valid():  # 会执行字段自己的校验(没有),执行局部钩子(没有),执行全局钩子(写了:校验用户,签发token)
            # ser.validated_data # 字典,校验过后的数据
            return Response(ser.validated_data )
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误11'})

"""-------------------序列化类--------------------"""

class LoginSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()

    # 写全局钩子
    def validate(self, attrs):
        # 校验用户,签发token
        username = attrs.get('username')
        password = attrs.get('password')
        # 2  使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
        if re.match(r'^1[3-9][0-9]{9}$', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(r'^.+@.+$', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user and user.check_password(password):
            # 3 校验密码
            # 4 签发token
            refresh = TokenObtainPairSerializer.get_token(user)
            data = {'code': 100,
                    'msg': '登录成功成功',
                    'username': self.user.username,
                    'refresh':str(refresh),
                    'access': str(refresh.access_token)
                    }
            return data
        else:
            raise ValidationError('用户名或密码错误')

五. jwt默认配置

# JWT配置
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),  # Access Token的有效期
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),  # Refresh Token的有效期
    
    # 对于大部分情况,设置以上两项就可以了,以下为默认配置项目,可根据需要进行调整
    
    # 是否自动刷新Refresh Token
    'ROTATE_REFRESH_TOKENS': False,  
    # 刷新Refresh Token时是否将旧Token加入黑名单,如果设置为False,则旧的刷新令牌仍然可以用于获取新的访问令牌。需要将'rest_framework_simplejwt.token_blacklist'加入到'INSTALLED_APPS'的配置中
    'BLACKLIST_AFTER_ROTATION': False,  
    'ALGORITHM': 'HS256',  # 加密算法
    'SIGNING_KEY': settings.SECRET_KEY,  # 签名密匙,这里使用Django的SECRET_KEY
    # 如为True,则在每次使用访问令牌进行身份验证时,更新用户最后登录时间
    "UPDATE_LAST_LOGIN": False, 
    # 用于验证JWT签名的密钥返回的内容。可以是字符串形式的密钥,也可以是一个字典。
    "VERIFYING_KEY": "",
    "AUDIENCE": None,# JWT中的"Audience"声明,用于指定该JWT的预期接收者。
    "ISSUER": None, # JWT中的"Issuer"声明,用于指定该JWT的发行者。
    "JSON_ENCODER": None, # 用于序列化JWT负载的JSON编码器。默认为Django的JSON编码器。
    "JWK_URL": None, # 包含公钥的URL,用于验证JWT签名。
    "LEEWAY": 0, # 允许的时钟偏差量,以秒为单位。用于在验证JWT的过期时间和生效时间时考虑时钟偏差。
    # 用于指定JWT在HTTP请求头中使用的身份验证方案。默认为"Bearer"
    "AUTH_HEADER_TYPES": ("Bearer",), 
    # 包含JWT的HTTP请求头的名称。默认为"HTTP_AUTHORIZATION"
    "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", 
     # 用户模型中用作用户ID的字段。默认为"id"。
    "USER_ID_FIELD": "id",
     # JWT负载中包含用户ID的声明。默认为"user_id"。
    "USER_ID_CLAIM": "user_id",
    
    # 用于指定用户身份验证规则的函数或方法。默认使用Django的默认身份验证方法进行身份验证。
    "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
    #  用于指定可以使用的令牌类。默认为"rest_framework_simplejwt.tokens.AccessToken"。
    "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
    # JWT负载中包含令牌类型的声明。默认为"token_type"。
    "TOKEN_TYPE_CLAIM": "token_type",
    # 用于指定可以使用的用户模型类。默认为"rest_framework_simplejwt.models.TokenUser"。
    "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
    # JWT负载中包含JWT ID的声明。默认为"jti"。
    "JTI_CLAIM": "jti",
    # 在使用滑动令牌时,JWT负载中包含刷新令牌过期时间的声明。默认为"refresh_exp"。
    "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
    # 滑动令牌的生命周期。默认为5分钟。
    "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
    # 滑动令牌可以用于刷新的时间段。默认为1天。
    "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
    # 用于生成access和刷refresh的序列化器。
    "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
    # 用于刷新访问令牌的序列化器。默认
    "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
    # 用于验证令牌的序列化器。
    "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
    # 用于列出或撤销已失效JWT的序列化器。
    "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
    # 用于生成滑动令牌的序列化器。
    "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
    # 用于刷新滑动令牌的序列化器。
    "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}



# 如何配置到我们项目的 settings.py 中
from datetime import timedelta
SIMPLE_JWT ={
    # Access Token的有效期
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    # Refresh Token的有效期
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    # token 前缀,默认是 Bearer
    "AUTH_HEADER_TYPES": ("TOKEN",),
}
posted @ 2023-04-11 12:19  codegjj  阅读(119)  评论(0编辑  收藏  举报