Loading

DRF访问控制(RBAC)、JWT认证

RBAC

  • 什么是RBAC

    • RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
    • 前面我们说到三大认证,比如权限,普通用户和管理员能够操作的接口就不一样,比如我们常见的视频软件,不开会员能看的视频寥寥无几,这就是权限的应用,但是这站在公司的角度是对外的权限,比如后台管理对公司内使用,公司内部的权限该如何分配?就用到了RBAC
    • 对外权限针对用户:普通注册用户,会员,超级会员····
    • 对内权限公司内部:使用RBAC的权限控制
      比如公司内部有开发部、财政部、市场部、人事部、运营部、总裁办···
      这写部门的权限是不同的,比如人事部有招人的权力,开发部有查看修改提交代码的权力···
      
      所以通过将权限和角色(部门)绑定,而角色又赋予用户,所以该部门有多大的权力,部门下的员工就有什么样的权力···
      
      总体而言,RBAC针对公司内部项目,后台管理开发居多
  • Django的内置RBAC(六表)

    • 图解
      • 权限三表
        image
      • 权限六表
        image
      • 表模型
        image
  • 表关系

    django的admin自带rbac权限管理(表设计完成权限管理),6张表

    • 用户表、组表(角色、部门)、权限表 ---> 三张
    • 用户和组多对多关系,存在中间表
    • 用户和权限多对多关系,存在中间表
    • 组和权限多对多关系,存在中间表 -----> 三张
      image
  • 实操

    • models.py
      from django.db import models
      
      
      class Book(models.Model):
          name = models.CharField(max_length=32)
          price = models.DecimalField(max_digits=5, decimal_places=2)
          author = models.CharField(max_length=32)
      
          # 对象描述,显示书名
          def __str__(self):
              return self.name
      
          # 表名中文解释
          class Meta:
              '''
              verbose_name 顾名思义 起一个复杂点的名称,一般用来作中文解释
      
              verbose_name_plural 顾名思义是一个复数名称,因中文没有复数
              但django有时又会将用户的驼峰命名拆成单个词,给最后的词加复数,和用户的本义不符,
              因些加了这样一个选项来处理尴尬 比如 Blog Articals 或是 分类管理s
              '''
              # verbose_name = '图书表'
              verbose_name_plural = '图书表'
      
    • admin
      from django.contrib import admin
      from .models import Book
      
      admin.site.register(Book)
      
  • 登录admin操作

    image
    image

    普通用户只能查看

    image

    添加到组里,增加修改权限#

    image
    image

  • admin二次开发

    初始样式

    image

    • admin.py
      from django.contrib import admin
      from .models import Book
      
      
      class BookAdmin(admin.ModelAdmin):
          # 设置列表可显示的字段
          list_display = ('name', 'price', 'author')
          # 设置过滤选项
          list_filter = ('name', 'price')
      
      
      admin.site.register(Book, BookAdmin)
      

    二次开发后的样式

    image

base64编码与解码

注意:base64长度必须是4的倍速,如果不够就使用=补齐,

import base64
import json

dic_info={
  "sub": "1234567890",
  "name": "HammerZe",
  "admin": True
}

'''base64编码'''
# 必须是bytes类型
s = json.dumps(dic_info).encode('utf8')
enbase64_str = base64.b64encode(s)
print(enbase64_str)
# b'eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogImxxeiIsICJhZG1pbiI6IHRydWV9'

'''base64解码'''
en_res = b'eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogImxxeiIsICJhZG1pbiI6IHRydWV9'

debase64_str = base64.b64decode(en_res).decode('utf8')
print(base64.b64decode(en_res),type(base64.b64decode(en_res)))
# b'{"sub": "1234567890", "name": "HammerZe", "admin": true}' <class 'bytes'>
print(debase64_str)
# {"sub": "1234567890", "name": "HammerZe", "admin": true}

JWT认证

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

  • 为什么使用JWT 认证?

    如果登录用户很多,需要在后端存很多数据,频繁查询数据库,导致效率低,JWT就可以使我们可以不在服务端存数据,又够保证数据安全,在客户端存数据 ----> token认证机制

    • Session机制

      image
      image
    • JWT机制

      image
      image
  • JWT的构成

    JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。比如:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    
    • JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存

      第一段头部承载的信息:

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

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

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

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

      eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
      
    • payload

      第二段是载荷,载荷就是存放有效信息的地方,是JWT的主体内容部分,也是一个JSON对象,承载的信息:

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

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

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

      公共的声明

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

      私有的声明

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

      除以上标准注册声明字段外,我们还可以自定义字段,一般会把包含用户信息的数据放到payload中,如下例:

      {
        "sub": "1234567890",
        "name": "HammerZe",
        "admin": true
      }
      

      注意

      虽然说用户信息数据可以存放到payload中,但是默认情况下JWT是未加密的,Base64算法也只是编码并不会提供安全的加密算法,一般程序员拿到Base64编码的字符串都可以解码出内容,所以不要存隐私信息,比如密码,防止泄露,存一些非敏感信息

    • signature

      签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名

      HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
      

      简单的说第三段是签证信息,这个签证信息由三部分组成:

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

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

      注意

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

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

      总结

      注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:
      
      header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
      
      signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值
      
  • 本质原理

    • 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对象就是合法的登录用户
      """
      
  • JWT的种类

    其实JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用JWT在两个组织之间传递安全可靠的信息,JWT的具体实现可以分为以下几种:

    • nonsecure JWT

      未经过签名,不安全的JWT。其header部分没有指定签名算法,并且也没有Signature部分

      {
        "alg": "none",
        "typ": "JWT"
      }
      
    • JWS

      JWS ,也就是JWT Signature,其结构就是在之前nonsecure JWT的基础上,在头部声明签名算法,并在最后添加上签名。创建签名,是保证jwt不能被他人随意篡改。我们通常使用的JWT一般都是JWS

      • 为了完成签名,除了用到header信息和payload信息外,还需要算法的密钥,也就是secretKey。加密的算法一般有2类:

        • 对称加密:secretKey指加密密钥,可以生成签名与验签
        • 非对称加密:secretKey指私钥,只用来生成签名,不能用来验签(验签用的是公钥)
        • JWT的密钥或者密钥对,一般统一称为JSON Web Key,也就是JWK
      • 到目前为止,jwt的签名算法有三种:

        • HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
        • RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
        • ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)
    • JWE

      payload部分经过加密的JWT

django中快速使用JWT

  • 签发

    一般我们登录成功后签发一个token串,token串分为三段,头部,载荷,签名

    1)用基本信息公司信息存储json字典,采用base64算法得到 头字符串
    2)用关键信息存储json字典,采用base64算法得到 荷载字符串,过期时间,用户id,用户名
    3)用头、体加密字符串通过加密算法+秘钥加密得到 签名字符串
    拼接成token返回给前台
    
  • 认证

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

    1)将token按 . 拆分为三段字符串,第一段 头部加密字符串 一般不需要做任何处理
    2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间是安全信息,确保token没过期
    3)再用 第一段 + 第二段 + 加密方式和秘钥得到一个加密串,与第三段 签名字符串 进行比较,通过后才能代表第二段校验得到的user对象就是合法的登录用户
    
  • JWT可以使用如下两种:

    djangorestframework-jwtdjangorestframework-simplejwt

  • django中快速使用JWT

    导入:pip3 install djangorestframework-jwt

    • 如何签发?

      • 路由中配置
        from rest_framework_jwt.views import obtain_jwt_token
        urlpatterns = [
            path('login/', obtain_jwt_token),
        ]
        
      • 使用接口测试工具发送post请求到后端,就能基于auth的user表签发token
        {
            "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IkhhbW1lciIsImV4cCI6MTY0OTUyNDY2MiwiZW1haWwiOiIifQ.P1Y8Z3WhdndHoWE0PjW-ygd53Ng0T46U04oY8_0StwI"
        }
        
        image
      • base64反解
        import base64
        
        # 第一段
        s1 = b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'
        print(base64.b64decode(s1))
        # b'{"typ":"JWT","alg":"HS256"}'
        
        # 第二段
        s2 = b'eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IkhhbW1lciIsImV4cCI6MTY0OTUyNDY2MiwiZW1haWwiOiIifQ=='
        print(base64.b64decode(s2))
        # b'{"user_id":1,"username":"Hammer","exp":1649524662,"email":""}'
        # 我们发现第二段可以反解密出用户信息,是有一定的风险,可以使用,但是不能更改,就好比你的身份证丢了,别人可以在你不挂失的情况下去网吧上网
        
        
        
        '''第三段不能不能反解,只能做base64解码,第三段使用base64编码只是为了统一格式'''
        
    • 如何认证?

      我们没有认证的时候,直接访问接口就可以返回数据,比如访问/books/发送GET请求就可以获取所有book信息,那么现在添加认证,需要访问通过才能访问才更合理

      • 步骤:
        • 视图中配置,必须配置认证类和权限类
        • 访问需要在请求头中使用,携带签发的token串,格式是:
          key是Authorization
          value是jwt token串
          Authorization : jwt token串
          '''注意jwt和token串中间有空格'''
          
      • 视图
        from rest_framework_jwt.authentication import JSONWebTokenAuthentication
        from rest_framework.permissions import IsAuthenticated
        class BookView(GenericViewSet,ListModelMixin):
            ···
             # JSONWebTokenAuthentication :rest_framework_jwt模块写的认证类
            authentication_classes = [JSONWebTokenAuthentication,]
            # 需要配合一个权限类
            permission_classes = [IsAuthenticated,]
            ···
        
        image
    • 定制签发token返回格式

      JWT默认的配置是,我们登录成功后只返回一个token串,这也是默认的配置,我们如果想签发token后返回更多数据需要我们自定制

      • 步骤
        • 写一个函数,返回什么格式,前端就能看见什么格式
        • 在配置文件中配置JWT_AUTH
      • utils.py
        # 定义签发token(登陆接口)返回格式
        def jwt_response_payload_handler(token, user=None, request=None):
            return {
                'code': 100,
                'msg': "登陆成功",
                'token': token,
                'username': user.username
            }
        
      • settings.py
        JWT_AUTH = {
              'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
          }
        
        image
  • JWT源码分析

    • 签发源码分析

      1.入口:path('login/', obtain_jwt_token)
      
      2.obtain_jwt_token--->obtain_jwt_token = ObtainJSONWebToken.as_view()
      ObtainJSONWebToken.as_view(),其实就是一个视图类.as_view()
      
      3.ObtainJSONWebToken类源码
      '''
      class ObtainJSONWebToken(JSONWebTokenAPIView):
          serializer_class = JSONWebTokenSerializer
      '''
      
      4.登录签发token肯定需要一个post方法出来,但是ObtainJSONWebToken类内没有父类JSONWebTokenAPIView写了post方法:
          def post(self, request, *args, **kwargs):
              # 获取数据:{'username': 'Hammer', 'password': '7410'}
              serializer = self.get_serializer(data=request.data)
              # 校验
              if serializer.is_valid():
                  user = serializer.object.get('user') or request.user # 获取用户
                  token = serializer.object.get('token') # 获取token
                  response_data = jwt_response_payload_handler(token, user, request) 
                  #  {'code': 100, 'msg': '登陆成功', 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IkhhbW1lciIsImV4cCI6MTY0OTU4MTU0NiwiZW1haWwiOiIifQ.2oAjKQ90SV2S9Yxrwppo7BwAOv0xFW4i4AHHBX5Cg2Q', 'username': 'Hammer'}
                  response = Response(response_data)
                  if api_settings.JWT_AUTH_COOKIE:
                     ···
                  return response # 定制什么返回什么
      
              return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
      5.get_serializer(data=request.data)如何获取到用户数据?
      JSONWebTokenSerializer序列化类中全局钩子中获取当前登录用户和签发token
      ···
      payload = jwt_payload_handler(user)
                      return {
                          'token': jwt_encode_handler(payload),
                          'user': user
                      }
      ···
      

      签发总结

      从obtain_jwt_token开始, 通过ObtainJSONWebToken视图类处理,其实是父类JSONWebTokenAPIView的post方法通过传入的用户名和密码处理获取当前用户,签发了token

    • 认证源码分析

      # 视图类内认证类搭配权限类使用
          authentication_classes = [JSONWebTokenAuthentication, ]
          permission_classes = [IsAuthenticated, ]
      

      我们在前面写过,如果需要认证肯定需要重写authenticate方法,这里从列表内的认证类作为入口分析:

      '''认证类源码'''
      class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
          www_authenticate_realm = 'api'
      
          def get_jwt_value(self, request):
              # 获取传入的Authorization:jwt token串,然后切分
              auth = get_authorization_header(request).split()
              auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()
              # 获取不到的情况
              if not auth:
                  if api_settings.JWT_AUTH_COOKIE:
                      return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
                  return None  # 直接返回None,也不会报错,所以必须搭配权限类使用
      
              ···
      
              return auth[1]  # 一切符合判断条件,通过split切分的列表索引到token串
      
      '''认证类父类源码'''
      def authenticate(self, request):
              jwt_value = self.get_jwt_value(request) # 获取真正的token,三段式,上面分析
              if jwt_value is None: # 如果没传token,就不认证了,直接通过,所以需要配合权限类一起用
                  return None
      
              try:
                  payload = jwt_decode_handler(jwt_value)# 验证签名
              except jwt.ExpiredSignature:
                  msg = _('Signature has expired.') # 过期了
                  raise exceptions.AuthenticationFailed(msg)
              except jwt.DecodeError:
                  msg = _('Error decoding signature.')# 被篡改了
                  raise exceptions.AuthenticationFailed(msg)
              except jwt.InvalidTokenError:
                  raise exceptions.AuthenticationFailed()# 不知名的错误
      
              user = self.authenticate_credentials(payload)
      
              return (user, jwt_value)
      
    • 签发源码内的其他两个类

      导入:from rest_framework_jwt.views import obtain_jwt_token,refresh_jwt_token,verify_jwt_token

      obtain_jwt_token = ObtainJSONWebToken.as_view()  # 获取token
      refresh_jwt_token = RefreshJSONWebToken.as_view()  # 更新token
      verify_jwt_token = VerifyJSONWebToken.as_view()  # 认证token
      

      refresh_jwt_token用法

      # 配置文件
      JWT_AUTH = {
          'JWT_ALLOW_REFRESH': True
      }
      
      # 路由
          path('refresh/', refresh_jwt_token)
      

      image

      verify_jwt_token用法

      path('verify/', verify_jwt_token),
      

      image

自定义User表,签发token

  • 普通写法,视图类写

    上面我们写道,签发token是基于Django自带的auth_user表签发,如果我们自定义User表该如何签发token,如下:

    • 视图
      # 自定义表签发token
      from rest_framework.views import APIView
      from rest_framework.viewsets import ViewSetMixin
      from rest_framework.decorators import action
      from rest_framework.response import Response
      from rest_framework_jwt.settings import api_settings
      from app01 import models
      class UserView(ViewSetMixin,APIView):
          @action(methods=['POST'],detail=False)
          def login(self,request):
              username = request.data.get('username')
              password = request.data.get('password')
              user = models.UserInfo.objects.filter(username=username,password=password).first()
              response_dict = {'code':None,'msg':None}
              # 源码copy错来使用
              jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
              jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
              if user:
                  '''
                  签发token去源码copy过来使用
                  '''
                  # 载荷字典
                  payload = jwt_payload_handler(user)
                  print(payload)
                  # {'user_id': 1, 'username': 'Hammer', 'exp': datetime.datetime(2022, 4, 10, 13, 13, 15, 363206), 'email': '123@qq.com', 'orig_iat': 1649596095}
                  # 通过荷载得到token串
                  token = jwt_encode_handler(payload)
                  response_dict['code'] = 2000
                  response_dict['msg'] = '登录成功'
                  response_dict['token'] = token
      
              else:
                  response_dict['code'] = 4001
                  response_dict['msg'] = '登录失败,用户名或密码错误'
              return Response(response_dict)
      
    • 模型
      # user表
      class UserInfo(models.Model):
          username = models.CharField(max_length=32)
          password = models.CharField(max_length=32)
          email = models.EmailField()
      
    • 路由
      from rest_framework.routers import SimpleRouter
      router = SimpleRouter()
      router.register('user',views.UserView,'user')
      
      image
  • 序列化类中写逻辑

    源码中签发校验都在序列化类中完成,这种写法确实比较常用,我们来使用这种方式自定义,将上面视图的校验逻辑写到序列化类中,这个序列化类只用来做反序列化,这样我们就可以利用 反序列化 的字段校验功能来帮助我们校验(模型中的条件),但是我们不做保存操作

    • 视图
      from .serializer import UserInfoSerializer
      class UserView(ViewSetMixin,APIView):
          @action(methods=['POST'],detail=False)
          def login(self,request):
              # 如果想获取什么这里可以实例化对象写入,比如request
              serializer = UserInfoSerializer(data=request.data, context={'request': request})
              response_dict = {'code':None,'msg':None}
              # 校验,局部钩子,全局钩子都校验完才算校验通过,走自己的校验规则
              if serializer.is_valid():
                  # 从序列化器对象中获取token和username
                 token = serializer.context.get('token')
                 username = serializer.context.get('username')
      
                 response_dict['code']=2000
                 response_dict['msg']='登录成功'
                 response_dict['token'] = token
                 response_dict['username'] = username
              else:
                  response_dict['code'] = 4001
                  response_dict['msg'] = '登录失败,用户名或密码错误'
      
              return Response(response_dict)
      
      
    • 序列化器
      from rest_framework.exceptions import ValidationError
      
      
      class UserInfoSerializer(serializers.ModelSerializer):
          class Meta:
              model = UserInfo
              # 根据模型里的字段写
              fields = ['username', 'password']
      
          # 全局钩子
          def validate(self, attrs):
              # attrs是校验过的字段,这里利用
              username = attrs.get('username')
              password = attrs.get('password')
              user = UserInfo.objects.filter(username=username, password=password).first()
      
              from rest_framework_jwt.settings import api_settings
              jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
              jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
      
              if user:  # 登录成功
      
                  payload = jwt_payload_handler(user)  # 得到荷载字典
                  token = jwt_encode_handler(payload)  # 通过荷载得到token串
                  # 将token放入context字典中
                  self.context['token'] = token
                  self.context['username'] = username
                  # context是serializer和视图类沟通的桥梁
                  print(self.context.get('request').method)
              else:  # 登录失败
                  raise ValidationError('用户名或密码错误')
              return attrs
      
      image
    • 总结

      需要我们注意的是,context只是我们定义的字典,比如上面写到的实例化序列化类中指定的context,那么就可以从序列化类打印出请求的方法,context是序列化类和视图类沟通的桥梁

  • 自定义认证类

    • auth.py

      import jwt
      from django.utils.translation import ugettext as _
      from rest_framework import exceptions
      from rest_framework.authentication import BaseAuthentication
      from rest_framework.exceptions import AuthenticationFailed
      from rest_framework_jwt.settings import api_settings
      from .models import UserInfo
      
      
      class JWTAuthentication(BaseAuthentication):
          def authenticate(self, request):
              # 第一步、取出传入的token,从请求头中取
      
              # 这里注意,获取的时候格式为:HTTP_请求头的key大写
              jwt_value = request.META.get('HTTP_TOKEN')
              jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
              # 验证token:验证是否过期,是否被篡改,是否有其他未知错误,从源码copy过来使用
              if jwt_value:
                  try:
                      payload = jwt_decode_handler(jwt_value)
                  except jwt.ExpiredSignature:
                      msg = _('Signature has expired.')
                      raise exceptions.AuthenticationFailed(msg)
                  except jwt.DecodeError:
                      msg = _('Error decoding signature.')
                      raise exceptions.AuthenticationFailed(msg)
                  except jwt.InvalidTokenError:
                      msg = _('Unknown Error.')
                      raise exceptions.AuthenticationFailed(msg)
      
                  # 第二部、通过payload获得当前登录用户,本质是用户信息通过base64编码到token串的第二段载荷中
                  user = UserInfo.objects.filter(pk=payload['user_id']).first()
                  # 返回user和token
                  return (user, jwt_value)
              else:
                  raise AuthenticationFailed('No token was detected')
      
    • 视图

      from rest_framework.viewsets import ModelViewSet
      from .models import Book
      from .serializer import BookSerializer
      from .auth import JWTAuthentication
      class BookView(ModelViewSet):
          queryset = Book.objects.all()
          serializer_class = BookSerializer
          authentication_classes = [JWTAuthentication,]
      
    • 序列化器

      class BookSerializer(serializers.ModelSerializer):
          class Meta:
              model = Book
              fields = '__all__'
      
    • 路由

      from rest_framework.routers import SimpleRouter
      router = SimpleRouter()
      router.register('book',views.BookView,'book')
      

      正常的情况

      image

      不携带token的情况

      image

      总结

      • 从请求头中获取token,格式是HTTP_KEY,key要大写
      • 认证token串没有问题,返回用户信息从载荷中获取,本质是用户信息通过base64编码到token串的第二段载荷中,可以通过base64解码获取到用户信息
  • 补充:HttpRequest.META

    HTTP请求的数据在META中

    HttpRequest.META
    一个标准的Python 字典,包含所有的HTTP 首部。具体的头部信息取决于客户端和服务器,下面是一些示例:
    :
    CONTENT_LENGTH —— 请求的正文的长度(是一个字符串)。
    CONTENT_TYPE —— 请求的正文的MIME 类型。
    HTTP_ACCEPT —— 响应可接收的Content-Type。
    HTTP_ACCEPT_ENCODING —— 响应可接收的编码。
    HTTP_ACCEPT_LANGUAGE —— 响应可接收的语言。
    HTTP_HOST —— 客服端发送的HTTP Host 头部。
    HTTP_REFERER —— Referring 页面。
    HTTP_USER_AGENT —— 客户端的user-agent 字符串。
    QUERY_STRING —— 单个字符串形式的查询字符串(未解析过的形式)。
    REMOTE_ADDR —— 客户端的IP 地址。
    REMOTE_HOST —— 客户端的主机名。
    REMOTE_USER —— 服务器认证后的用户。
    REQUEST_METHOD —— 一个字符串,例如"GET" 或"POST"。
    SERVER_NAME —— 服务器的主机名。
    SERVER_PORT —— 服务器的端口(是一个字符串)。
    上面可以看到,除 CONTENT_LENGTH 和 CONTENT_TYPE 之外,请求中的任何 HTTP 首部转换为 META 的键时,
    都会将所有字母大写并将连接符替换为下划线最后加上 HTTP_  前缀。
    所以,一个叫做 X-Bender 的头部将转换成 META 中的 HTTP_X_BENDER 键。
    

Simple UI快速上手

在混合开发的模式下,如果想使用django admin,又嫌弃后台不符合你的审美?Simple UI给你想要的答案

Simple UI官方文档:传送门

  • 支持django和python版本

    • 支持的Python版本 >= 2.7
    • 支持的Django版本 >= 2.x
  • 安装

    pip3 install django-simpleui

  • 注册

    在自己项目的settings.py文件中INSTALLED_APPS的第一行加入simpleui

      # Application definition
      INSTALLED_APPS = [
          'simpleui',
          'django.contrib.admin',
          'django.contrib.auth',
          'django.contrib.contenttypes',
          'django.contrib.sessions',
          'django.contrib.messages',
          'django.contrib.staticfiles',
          ...
      ]
    
  • 模型

    from django.db import models
    
    
    # Create your models here.
    
    class Book(models.Model):
        name = models.CharField(max_length=32,help_text='书名')
        price = models.DecimalField(max_digits=5, decimal_places=2,help_text='价钱')
        author = models.CharField(max_length=32,help_text='作者')
    
        # 对象描述,显示书名
        def __str__(self):
            return self.name
    
        # 表名中文解释
        class Meta:
            '''
            verbose_name 顾名思义 起一个复杂点的名称,一般用来作中文解释
    
            verbose_name_plural 顾名思义是一个复数名称,因中文没有复数
            但django有时又会将用户的驼峰命名拆成单个词,给最后的词加复数,和用户的本义不符,
            因些加了这样一个选项来处理尴尬 比如 Blog Articals 或是 分类管理s
            '''
            # verbose_name = '图书表'
            verbose_name_plural = '图书表'
    
  • admin

    from django.contrib import admin
    
    from .models import Book
    
    
    # Register your models here.
    
    class BookAdmin(admin.ModelAdmin):
        # 设置列表可显示的字段
        list_display = ('name', 'price', 'author')
        # 设置过滤选项
        list_filter = ('name', 'price')
    
    
    admin.site.register(Book, BookAdmin)
    
    
  • 测试

    image
    image

  • 自定义菜单

    import time
    SIMPLEUI_CONFIG = {
        'system_keep': False,
        'menu_display': ['Simpleui', '测试', '权限认证', '动态菜单测试'],      # 开启排序和过滤功能, 不填此字段为默认排序和全部显示, 空列表[] 为全部不显示.
        'dynamic': True,    # 设置是否开启动态菜单, 默认为False. 如果开启, 则会在每次用户登陆时动态展示菜单内容
        'menus': [{
            'name': 'Simpleui',
            'icon': 'fas fa-code',
            'url': 'https://gitee.com/tompeppa/simpleui'
        }, {
            'app': 'auth',
            'name': '权限认证',
            'icon': 'fas fa-user-shield',
            'models': [{
                'name': '用户',
                'icon': 'fa fa-user',
                'url': 'auth/user/'
            }]
        }, {
            # 自2021.02.01+ 支持多级菜单,models 为子菜单名
            'name': '多级菜单测试',
            'icon': 'fa fa-file',
            # 二级菜单
            'models': [{
                'name': 'Baidu',
                'icon': 'far fa-surprise',
                # 第三级菜单 ,
                'models': [
                    {
                      'name': '爱奇艺',
                      'url': 'https://www.iqiyi.com/dianshiju/'
                      # 第四级就不支持了,element只支持了3级
                    }, {
                        'name': '百度问答',
                        'icon': 'far fa-surprise',
                        'url': 'https://zhidao.baidu.com/'
                    }
                ]
            }, {
                'name': '内网穿透',
                'url': 'https://www.wezoz.com',
                'icon': 'fab fa-github'
            }]
        }, {
            'name': '动态菜单测试' ,
            'icon': 'fa fa-desktop',
            'models': [{
                'name': time.time(),
                'url': 'http://baidu.com',
                'icon': 'far fa-surprise'
            }]
        }]
    }
    
  • 快速引用静态文件配置

    STATICFILES_DIRS = [
        os.path.join(BASE_DIR, 'static'),
    ]
    
posted @ 2022-11-05 17:54  爱learn  阅读(267)  评论(0编辑  收藏  举报