JWT认证

一、JWT认证

1、简介

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。

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

2、构成和工作原理

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

2.1、 header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

2.2、payload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

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

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避时序攻击。

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

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

定义一个payload:

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

然后将其进行base64加密,得到JWT的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

2.3、signature

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

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

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。

3、本质原理

jwt认证算法:签发与校验

"""
1)jwt分三段式:头.体.签名 (head.payload.sgin)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的
3)头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息
{
	"company": "公司信息",
	...
}
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间
{
	"user_id": 1,
	...
}
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就能访问登录的用户

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

二、drf-jwt安装和简单使用

1、官网

http://getblimp.github.io/django-rest-framework-jwt/

2、安装

pip install djangorestframework-jwt -i https://pypi.douban.com/simple/

3、基本使用

1、继承AbstractUser表
# models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    phone = models.CharField(max_length=11)
    icon = models.ImageField(upload_to='icon')  # ImageField依赖于pillow模块
   
2、配置扩展配置
# settions.py
# 扩展user表
AUTH_USER_MODEL = 'api.user'

# 配置头像相关
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')  # 并且在根目录创建media目录

3、创建超级用户并做数据库迁移
python3 manage.py makemigrations
python3 manage.py migrate

# 创建超级用户
createsuperuser

4、基本使用
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),  # 基本使用,快速签发token
]

5、用postman测试
	向后端接口发送post请求,携带用户名密码,即可看到生成的token

6、在settions中注册
INSTALLED_APPS = [
	'rest_framework_jwt'
]

4、补充base64

import base64
import json

dic = {'name': 'meng', 'age': 18}
dic_str = json.dumps(dic)
# 编码(字符串,json格式字符串
res = base64.b64encode(dic_str.encode('utf-8'))
print(res)

# 解码
res1 = base64.b64decode(res)
print(res1)

# 输出结果
b'eyJuYW1lIjogIm1lbmciLCAiYWdlIjogMTh9'
b'{"name": "meng", "age": 18}'

三、JWT自定义类

1、urls.py

from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token  # 导入类
from api import views
urlpatterns = [
    path('admin/', admin.site.urls),
    # 登录认证的token
    path('login/', obtain_jwt_token),  # 使用
    path('books/', views.BookView.as_view()),
]

2、views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from api.auth import MyToken  # 自定义类
from rest_framework.permissions import IsAuthenticated  # 内置权限类


class BookView(APIView):
    # 用内置认证类
    # authentication_classes = [JSONWebTokenAuthentication]  # 想要使用内置必须配置下面的
    # 权限控制
    # permission_classes = [IsAuthenticated]  # 当时访问的时候,Authorization:JWT(空格) 字符串,必须以JWT开头才可以解析
    
    authentication_classes = [MyToken]  # 用自己写的认证类

    def get(self, request):
        print(request.user)
        return Response('ok')

3、auth.py(自定义认证类)

from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication,BaseJSONWebTokenAuthentication
from rest_framework import exceptions


# 自写认证类
class MyToken(BaseJSONWebTokenAuthentication):  # 继承这个,authenticate_credentials方法
    def authenticate(self, request):  # 需要重写authenticate方法
        jwt_value = str(request.META.get('HTTP_AUTHORIZATION'))  # 取出随机字符串
        # 认证
        try:
            payload = jwt_decode_handler(jwt_value)  # 去比对随机字符串是否正确,错误抛异常
        except Exception:
            raise exceptions.AuthenticationFailed('认证失败')
        user = self.authenticate_credentials(payload)  # 得到一个user对象,用这个方法转成user对象
        return payload, None   # 认证通过返回两个值

jwt空格+字符串

四、JWT

1、控制用户登录才能访问

1.1、urls.py

from django.urls import path, include,re_path
from rest_framework_jwt.views import obtain_jwt_token
from app02 import views
urlpatterns = [
    path('login/', obtain_jwt_token),
    path('order/', views.OrderView.as_view()),  # 登录才能访问
    path('info/', views.UserInfoView.as_view()),  # 不登录就可以访问
]

1.2、views.py

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated  # 内置权限类


# 可以通过认证类和权限控制类IsAuthenticated,控制用户登录后才能访问的接口
# 如果用户不登录就可以访问,只需要把权限类去掉就可以
class OrderView(APIView):
    authentication_classes = [JSONWebTokenAuthentication]  # 认证类
    # 权限控制
    permission_classes = [IsAuthenticated]
    def get(self, request, *args, **kwargs):
        return Response('这是订单信息')
    
class UserInfoView(APIView):
    authentication_classes = [JSONWebTokenAuthentication]  # 认证类
    def get(self, request, *args, **kwargs):
        return Response('hihi')

2、控制登录接口返回的数据格式

# 第一种方法:用内置方法,控制登录接口返回的数据格
	-jwt的配置信息中有这个属性
        'JWT_RESPONSE_PAYLOAD_HANDLER':
        'rest_framework_jwt.utils.jwt_response_payload_handler',
    # 重写jwt_response_payload_handler,可以写什么返回什么

2.1、自定义utils.py

def my_jwt_response_payload_handler(token, user=None, request=None):  # 返回什么,前端就看到什么
    return {
        'token': token,
        'msg': '成功',
        'status': 100,
        'username': user.username
    }

2.2、settion.py配置

# 配置JWT
JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'app02.utils.my_jwt_response_payload_handler'
}

3、自定义JWT认证类

3.1、utils.py

from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt
from api.models import User

# 这两个其实是一个
from rest_framework_jwt.authentication import jwt_decode_handler  # 用来反射去使用下面的
from rest_framework_jwt.utils import jwt_decode_handler
'''
1、进入BaseJSONWebTokenAuthentication 到JSONWebTokenAuthentication类中
     def get_jwt_value(self, request):
        auth = get_authorization_header(request).split()  # get_authorization_header方法
    拿到auth = request.META.get('HTTP_AUTHORIZATION', b'')
'''
class MyJwtAuthentication(BaseAuthentication):
    def authenticate(self, request):
        jwt_value = request.META.get('HTTP_AUTHORIZATION')
        if jwt_value:
            # 使用jwt提供通过三段token,取出payload方法,并且有校验功能
            try:
                payload = jwt_decode_handler(jwt_value)
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('签名过期')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('非法用户')
            except Exception as e:
                raise AuthenticationFailed(str(e))  # 所有的异常都会走到这里
            print(payload)
            # return payload, jwt_value
            # 需要得到user对象,第一种求取数据库里查询
            # user = User.objects.get(pk=payload.get('user_id'))

            # 第二种:不需要查询数据库
            user = User(id=payload.get('user_id'), username=payload.get('username'))
            return user, jwt_value
        # 没有值,直接抛异常
        raise AuthenticationFailed('您没有携带认证信息')

基于BaseJSONWebTokenAuthentication写认证类

from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt
from api.models import User

# 这两个其实是一个
from rest_framework_jwt.authentication import jwt_decode_handler  # 用来反射去使用下面的
from rest_framework_jwt.utils import jwt_decode_handler

class MyJwtAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        jwt_value = request.META.get('HTTP_AUTHORIZATION')
        if jwt_value:
            # 使用jwt提供通过三段token,取出payload方法,并且有校验功能
            try:
                payload = jwt_decode_handler(jwt_value)
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('签名过期')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('非法用户')
            except Exception as e:
                raise AuthenticationFailed(str(e))  # 所有的异常都会走到这里

            # user=User.objects.get(id=payload['user_id'])  也可以这样
            user = self.authenticate_credentials(payload)  # 得到一个user对象
            return user, jwt_value
        # 没有值,直接抛异常
        raise AuthenticationFailed('您没有携带认证信息')

3.2、urls.py

from django.urls import path, include,re_path
from rest_framework_jwt.views import obtain_jwt_token
from app02 import views
urlpatterns = [
    path('login/', obtain_jwt_token),
    path('order/', views.OrderView.as_view()),
    path('info/', views.UserInfoView.as_view()),
    path('info2/', views.UserInfo2View.as_view()),
]

3.3、views.py(局部使用和全局使用)

# 局部使用
from app02.utils import MyJwtAuthentication
class UserInfo2View(APIView):
    authentication_classes = [MyJwtAuthentication]  # 自定义认证类

    def get(self, request, *args, **kwargs):
        print(request.user)
        return Response('认证信息')
    
# 全局使用,在配置文件中配置
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES':['app01.utils.MyJwtAuthentication'], # 全局生效
}

# 注:
    配置的所有认证,权限,频率。。。优先用视图类自己的,再用配置文件的,最后用drf内置的

五、多方式登录(手动签发token)

使用用户名,手机号,邮箱都可以登录

1、views.py

from app02.serializer import LoginModelSerializer
from rest_framework.viewsets import ViewSet


# 手动签发Token,完成多方式登录
class LoginView(ViewSet):
    def login(self, request, *args, **kwargs):
        login_ser = LoginModelSerializer(data=request.data, context={'request': request})
        login_ser.is_valid(raise_exception=True)
        token = login_ser.context.get('token')
        return Response({'status': 100, 'msg': '登录成功', "token": token, 'username': login_ser.context.get('username')})

2、urls.py

from django.urls import path, include, re_path
from rest_framework_jwt.views import obtain_jwt_token
from app02 import views

urlpatterns = [
    path('login/', obtain_jwt_token),
    path('order/', views.OrderView.as_view()),
    path('info/', views.UserInfoView.as_view()),
    path('info2/', views.UserInfo2View.as_view()),
    path('login2/', views.LoginView.as_view({'post': 'login'})),
]

3、serializer.py

from rest_framework import serializers
from api.models import User
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler, jwt_payload_handler
import re


class LoginModelSerializer(serializers.ModelSerializer):
    # 登录请求,走的是post方法,默认post方法完成的是create入库校验,所以唯一约束的字段,会进行数据库唯一校验,导致逻辑相悖
    # 需要覆盖系统字段,自定义校验规则,就可以避免完成多余的不必要校验,如唯一字段校验
    username = serializers.CharField()

    class Meta:
        model = User
        fields = ['username', 'password']

    # 全局钩子
    def validate(self, attrs):
        print(self.context)
        username = attrs.get('username')  # 用户名有三种方式
        password = attrs.get('password')
        print(password)

        if re.match(r'^1[3-9][0-9]{9}$', username):
            # 手机登录
            user = User.objects.filter(phone=username).first()
        elif re.match(r'^.+@.+$', username):
            # 邮箱登录
            user = User.objects.filter(email=username).first()
        else:
            # 账号登录
            user = User.objects.filter(username=username).first()
        if user:
            if user.check_password(password):
                # 签发token
                payload = jwt_payload_handler(user)  # 把user传入,得到payload
                token = jwt_encode_handler(payload)  # 把payload传入,得到token
                self.context['token'] = token
                self.context['username'] = username
                print(1)
                return attrs
            else:
                raise ValidationError('密码错误')
        else:
            raise ValidationError('用户不存在')

六、配置过期时间

1、settions.py配置

import datetime
# 配置JWT
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),  # 过期时间手动配置
}
posted @   迷恋~以成伤  阅读(240)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示