drf10 jwt

Django REST framework JWT

一、jwt概述

1. jwt介绍

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

很多公司开发的一些移动端可能不支持cookie,并且我们通过cookie和session做接口登录认证的话,效率其实并不是很高,我们的接口可能提供给多个客户端,session数据保存在服务端,那么就需要每次都调用session数据进行验证,比较耗时,所以引入了token认证的概念,我们也可以通过token来完成,我们来看看jwt是怎么玩的。

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

看图

2. JWT的构成

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

2.1 header

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

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

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

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

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

python中base64加密解密

import base64
str1 = 'admin'
str2 = str1.encode()
b1 = base64.b64encode(str2) #数据越多,加密后的字符串越长
b2 = base64.b64decode(b1) #admin
各个语言中都有base64加密解密的功能,所以我们jwt为了安全,需要配合第三段加密

2.2 payload

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

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

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

  • iss: jwt签发者

  • sub: jwt所面向的用户

  • aud: 接收jwt的一方

  • exp: jwt的过期时间,这个过期时间必须要大于签发时间

  • nbf: 定义在什么时间之前,该jwt都是不可用的.

  • iat: jwt的签发时间

  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

    以上是JWT 规定的7个官方字段,供选用

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

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

定义一个payload,json格式的数据:

import json
import base64
str1 = {
  "sub": "1234567890",
  "exp": "3422335555", #时间戳形式
  "name": "John Doe",
  "admin": true
}

str1 = json.dumps(str1)
str2 = str1.encode('utf8')
b1 = base64.b64encode(str2) #数据越多,加密后的字符串越长
b2 = base64.b64decode(b1) #admin

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

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

载荷就是用来承载一些可用信息的. 标准声明中有过期日期设置等等.

2.3 signature

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

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

这个部分需要base64加密后的headerbase64加密后的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的优点:
1. 实现分布式的单点登陆非常方便
2. 数据实际保存在客户端,所以我们可以分担服务器的存储压力
3. JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。

jwt的缺点:
1. 数据保存在了客户端,我们服务端只认jwt,不识别客户端。
2. jwt可以设置过期时间,但是因为数据保存在了客户端,所以对于过期时间不好调整。#secret_key轻易不要改,一改所有客户端都要重新登录

认证流程图

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

二、安装与使用

1. 安装

pip install djangorestframework-jwt -i https://mirrors.aliyun.com/pypi/simple/
    
django3+ 推荐使用
pip install djangorestframework-simplejwt 

2. 简单使用

# urls.py
from rest_framework_jwt.views import  obtain_jwt_token
    path('login/', obtain_jwt_token),
    
# settings.py
INSTALLED_APPS = [
     'rest_framework_jwt' # 注册
]

测试如下图

3. 基于auth的全局使用

# settings配置
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

# 设置 JWT_AUTH - JWT_EXPIRATION_DELTA 指明token的有效期
import datetime
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 
}

我们django创建项目的时候,在settings配置文件中直接就给生成了一个serect_key,我们直接可以使用它作为我们jwt的serect_key,其实djangorestframework-jwt默认配置中就使用的它。

4.基于auth的局部使用

from django.shortcuts import render
from app05_api import models
from app05_api import ser
from rest_framework.viewsets import ModelViewSet
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated

class BookModelViewSet(ModelViewSet):
    queryset = models.Book.objects.all()
    serializer_class = ser.BookModelSerializer
    # 认证控制 - 登录才能进
    authentication_classes = [JSONWebTokenAuthentication,]
    # 权限控制 - 只有登录才能访问
    permission_classes = [IsAuthenticated,]

5.控制jwt返回的数据格式

编写函数

# utils.py
# 控制jwt返回的数据格式
 # 返回什么,前端就能看到什么样子 - 返回的是登录成功的结果
def my_jwt_response_payload_handler(token, user=None, request=None):
    return {
        'token': token,
        'msg':'登录成功',
        'status':100,
        'username':user.username

    }

settings中配置

import datetime
JWT_AUTH = {
    # 响应格式
    'JWT_RESPONSE_PAYLOAD_HANDLER':'app08_jwt.utils.my_jwt_response_payload_handler',
    # 过期时间
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}

6.自定义jwt权限类 - 含解码

编写函数

from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication  # 基于它
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.utils import jwt_payload_handler, jwt_decode_handler, jwt_get_secret_key
import jwt
from django.contrib.auth import models


class MyJwtAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        # 从请求中获取 HTTP_AUTHORIZATION
        jwt_value = request.META.get('HTTP_AUTHORIZATION')
        if jwt_value:
            try:
                # jwt提供了通过三段token,取出payload的方法,并且有校验功能
                 if jwt_value.startswith('JWT ') or  jwt_value.startswith('JWT '):
                    jwt_value = jwt_value[4:]
                payload = jwt_decode_handler(jwt_value)
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('签名过期')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('非法用户')
            except Exception as e:
                raise AuthenticationFailed(str(e))
            # 如果走到这,说明已经获取到 payload

            # 获取用户对象
            # 方式一: authenticate_credentials 获取到荷载的信息
            user = self.authenticate_credentials(payload)
            # 方式二: 去数据库查询用户
            # user = models.User.objects.get(pk=payload.get('user_id'))
            # user = models.User.objects.filter(username=payload.get('username')).first()
            # 方式三: 不查库 - 直接获取这个对象
            # user = models.User(username=payload.get('username'))

            # 返回user对象 token
            return (user, jwt_value)
        raise AuthenticationFailed('您没有携带认证信息')
        
# 方式二: 去数据库查询用户 - 可能需要继承 AbstractUser
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
    phone=models.CharField(max_length=11)
    icon=models.ImageField(upload_to='icon')  # ImageField依赖于pillow模块
    # pip install pillow

在settings中配置

 REST_FRAMEWORK = {
    # Authentication
     'DEFAULT_AUTHENTICATION_CLASSES': [
         # jwt自定义认证
         'app08_jwt.utils.MyJwtAuthentication',
   
     ],}

视图类 - 局部使用

from django.shortcuts import render
from app05_api import models
from app05_api import ser
from rest_framework.viewsets import ModelViewSet
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from app08_jwt.utils import MyJwtAuthentication
from rest_framework.permissions import IsAuthenticated
class BookModelViewSet(ModelViewSet):
    queryset = models.Book.objects.all()
    serializer_class = ser.BookModelSerializer
    # 认证控制 - 登录才能进
    authentication_classes = [MyJwtAuthentication,]
    # 权限控制 - 只有登录才能访问
    permission_classes = [IsAuthenticated,]

7.手动签发token - 多方式登录

模型层

这里直接用auth 不然还得涉及到数据库迁移


from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
    phone=models.CharField(max_length=11)
    icon=models.ImageField(upload_to='icon')  # ImageField依赖于pillow模块
    # pip install pillow

序列化

基于auth

from rest_framework import serializers
from django.contrib.auth import models
import re
from rest_framework.exceptions import ValidationError

from rest_framework_jwt.utils import jwt_encode_handler,jwt_payload_handler

class LoginModelSerializer(serializers.ModelSerializer):
    username=serializers.CharField()  # 重新覆盖username字段,数据中它是unique,post,认为你保存数据,自己有校验没过
    class Meta:
        model=models.User
        fields=['username','password']

    def validate(self, attrs):

        print(self.context)

        # 在这写逻辑
        username=attrs.get('username') # 用户名有三种方式
        password=attrs.get('password')
        # 通过判断,username数据不同,查询字段不一样
        # 正则匹配,如果是手机号
        if re.match('^1[3-9][0-9]{9}$',username):
            user=models.User.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$',username):# 邮箱
            user=models.User.objects.filter(email=username).first()
        else:
            user=models.User.objects.filter(username=username).first()
        if user: # 存在用户
            # 校验密码,因为是密文,要用check_password
            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']=user.username
                return attrs
            else:
                raise ValidationError('密码错误')
        else:
            raise ValidationError('用户不存在')


视图层

from rest_framework.viewsets import ViewSet
from app08_jwt.utils import MyJwtAuthentication
from django.contrib.auth.models import User
from app08_jwt.serializers import LoginModelSerializer
from rest_framework.decorators import action
from rest_framework.response import  Response

class LoginViewSet(ViewSet):
    queryset = User.objects.all()
    serializer_class = LoginModelSerializer

    @action(methods=['post'],detail=False,url_path='login')
    def login_test(self,request,*args,**kwargs):
        print('123')
        # 1 需要 有个序列化的类
        login_ser = LoginModelSerializer(data=request.data, context={'request': request})
        # 2 生成序列化类对象
        # 3 调用序列号对象的is_validad
        login_ser.is_valid(raise_exception=True)
        token = login_ser.context.get('token')
        # 4 return
        return Response(
            {'status': 100, 'msg': '登录成功', 'token': token, 'username': login_ser.context.get('username')})


路由层

from django.urls import path, re_path, include
from rest_framework import routers
from app08_jwt import views
router = routers.SimpleRouter()
router.register('log_in', views.LoginModelSerializer)  # 不用加后缀
urlpatterns = [
    re_path('', include(router.urls)),
]

验证

# 使用用户名,手机号,邮箱,都可以登录#
# 前端需要传的数据格式
{
"username":"admin/123@qq.com/13345678910", # 这里没有用手机号,需要扩展后使用
"password":"admin123"
}

三、auth权限管理

# RBAC :是基于角色的访问控制(Role-Based Access Control ),公司内部系统
# django的auth就是内置了一套基于RBAC的权限系统

# django中
	# 后台的权限控制(公司内部系统,crm,erp,协同平台)
	user表
    permssion表
    group表
    user_groups表是user和group的中间表
    group_permissions表是group和permssion中间表
    user_user_permissions表是user和permission中间表
    # 前台(主站),需要用三大认证
# 演示:
	

四、补充

前端实现登陆功能

在登陆组件中找到登陆按钮,绑定点击事件

<button class="login_btn" @click="loginhander">登录</button>

在methods中请求后端

export default {
  name: 'Login',
  data(){
    return {
        login_type: 0,
        remember:false, // 记住密码
        username:"",
        password:"",
    }
  },

  methods:{
    // 登录
    loginhander(){
      this.$axios.post("http://127.0.0.1:8000/users/authorizations/",{"username":this.username,"password":this.password}).then(response=>{
        console.log(response.data)
      }).catch(error=>{
        console.log(error)
      })
    }
  },

};

前端保存jwt

我们可以将JWT保存在cookie中,也可以保存在浏览器的本地存储里,我们保存在浏览器本地存储中

浏览器的本地存储提供了sessionStorage 和 localStorage 两种,从属于window对象:

  • sessionStorage 浏览器关闭即失效
  • localStorage 长期有效

使用方法

sessionStorage.变量名 = 变量值   // 保存数据
sessionStorage.setItem("变量名","变量值") // 保存数据
sessionStorage.变量名  // 读取数据
sessionStorage.getItem("变量名") // 读取数据
sessionStorage.removeItem("变量名") // 清除单个数据
sessionStorage.clear()  // 清除所有sessionStorage保存的数据

localStorage.变量名 = 变量值   // 保存数据
localStorage.setItem("变量名","变量值") // 保存数据
localStorage.变量名  // 读取数据
localStorage.getItem("变量名") // 读取数据
localStorage.removeItem("变量名") // 清除单个数据
localStorage.clear()  // 清除所有sessionStorage保存的数据

登陆组件代码Login.vue

	// 使用浏览器本地存储保存token
  if (this.remember) {
    // 记住登录
    sessionStorage.clear();
    localStorage.token = response.data.token;
  } else {
    // 未记住登录
    localStorage.clear();
    sessionStorage.token = response.data.token;
  }
	// 页面跳转回到上一个页面 也可以使用 this.$router.push("/") 回到首页
	this.$router.go(-1)

默认的返回值仅有token,我们还需在返回值中增加username和id,方便在客户端页面中显示当前登陆用户

通过修改该视图的返回值可以完成我们的需求。

在user/utils.py 中,创建

def jwt_response_payload_handler(token, user=None, request=None):
    """
    自定义jwt认证成功返回数据
    """
    return {
        'token': token,
        'id': user.id,
        'username': user.username
    }

修改settings/dev.py配置文件

# JWT
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}

登陆组件代码Login.vue

	// 使用浏览器本地存储保存token
  if (this.remember) {
    // 记住登录
    sessionStorage.clear();
    localStorage.token = response.data.token;
    localStorage.id = response.data.id;
    localStorage.username = response.data.username;
  } else {
    // 未记住登录
    localStorage.clear();
    sessionStorage.token = response.data.token;
    sessionStorage.id = response.data.id;
    sessionStorage.username = response.data.username;
  }
	//登录成功之后,提示用户,然后用户点击确定,就跳转到首页
	let ths = this;
  this.$alert('登录成功!','路飞学城',{  #element-ui的alert需要三个参数,提示信息,标题,{callback回调函数}
    callback(){
      ths.$router.push('/');  
    }
  })

校验token的有效性

checklogin(){
      if (localStorage.token){

        this.token = localStorage.token
        this.where = 1;
      }
      else if (sessionStorage.token){
        this.token = sessionStorage.token
        this.where = 0;
      }else {

        return false;

      }
      // this.token = localStorage.token || sessionStorage.token;
      this.$axios.post(`${this.$settings.host}/users/verify_token/`,{
        token:this.token,
      })
      .then((res)=>{
        if (this.where === 0){
          sessionStorage.token = res.data.token;
        }else {
          localStorage.token = res.data.token;
        }
      })
      .catch((error)=>{
        this.$confirm('请重新登录', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.$router.push('/login');
        }).catch(()=>{
          this.token = false;
        })
      })

    },

后端url配置,在users应用下面的urls.py文件中

from django.urls import path,re_path
from rest_framework_jwt.views import obtain_jwt_token,verify_jwt_token
from . import views
urlpatterns = [
    path('login/', views.ObtainAPIView.as_view()),
    path('verify/', verify_jwt_token),
]

多条件登录

JWT扩展的登录视图,在收到用户名与密码时,也是调用Django的认证系统中提供的authenticate()来检查用户名与密码是否正确。

我们可以通过修改Django认证系统的认证后端(主要是authenticate方法)来支持登录账号既可以是用户名也可以是手机号。

官方说:修改Django认证系统的认证后端需要继承django.contrib.auth.backends.ModelBackend,并重写authenticate方法。

authenticate(self, request, username=None, password=None, **kwargs)方法的参数说明:

  • request 本次认证的请求对象
  • username 本次认证提供的用户账号
  • password 本次认证提供的密码

我们想要让用户既可以以用户名登录,也可以以手机号登录,那么对于authenticate方法而言,username参数即表示用户名或者手机号。

重写authenticate方法的思路:

  1. 根据username参数查找用户User对象,username参数可能是用户名,也可能是手机号
  2. 若查找到User对象,调用User对象的check_password方法检查密码是否正确

在users应用下创建一个utils.py中编写:

def get_user_by_account(account):
    """
    根据帐号获取user对象
    :param account: 账号,可以是用户名,也可以是手机号
    :return: User对象 或者 None
    """
    try:

        user = models.User.objects.filter(Q(username=account)|Q(mobile=account)).first()
    except models.User.DoesNotExist:
        return None
    else:
        return user

from . import models
from django.db.models import Q
from django.contrib.auth.backends import ModelBackend
class UsernameMobileAuthBackend(ModelBackend):
    """
    自定义用户名或手机号认证
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        user = get_user_by_account(username)
        #if user is not None and user.check_password(password) :
        if user is not None and user.check_password(password) and user.is_authenticated:
            #user.is_authenticated是看他有没有权限的,这里可以不加上它
            return user

在配置文件settings/dev.py中告知Django使用我们自定义的认证后端

AUTHENTICATION_BACKENDS = [
    'users.utils.UsernameMobileAuthBackend',
]

以上就实现了我们通过用户名或者手机号的一个多条件登录。

posted @   派森的猫  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示