DRF初识攻坚(四)-----身份认证、权限分配、频率限制

一、认证原理

1.1 基础定义

  • Http特性:HTTP 是一种"无状态"协议,这意味着每次客户端检索网页时,客户端打开一个单独的连接到 Web 服务器,服务器不保留客户端之前请求的任何记录
  • Cookie定义:存储在客户端计算机上(一般是浏览器中)的文本文件,一般为一段字符串,并保留了各种跟踪信息,其为现阶段解决http无状态的基石,cookie泄漏即身份泄漏,所以通常cookie使用HTTPS方式传输
  • 认证意义:主要为确定当前发过来的请求(request)的人的身份,根据需要每次请求都验证

1.2 认证种类

1.2.1 session认证

  • cookie类认证四个步骤:
    在这里插入图片描述
  • session认证:常规基于cookie认证,服务器设置session表,验证通过后,在表中生成身份和随机cookie对应关系,客户端只保存随机cookie用于通信验证
  • 基于cookie认证弊端:前后端分离反向代理、移动端、小程序,支持均不理想,且均需要复杂后端设置,服务器端均需要生成cookie表,占内存,占硬盘

1.2.2 jwt认证

  • token认证
    在这里插入图片描述
  • jwt认证
    • 定义:json web token,token认证主力,且现在最流行
    • 特点:服务器端仅保存生成和验证token的算法即可,每次请求只验证token自身是否被篡改合法即可
    • 注意:前端保存token位置方式由前端确定,后端到相应位置提取token

二、jwt认证深入及案例

2.1 流程详解

JWT的认证流程如下:

  • 首次认证段
    1. 首先,前端post请求将用户名和密码发送到后端接口。常规是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探;
    2. 后端核对用户名和密码成功后,生成JWT Token;
    3. 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可;
  • 正常通信段
    1. 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
    2. 后端查验前端传过来的JWT Token,比如签名是否正确、是否过期、token的接收方是否是自己等等
    3. 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

JWT Token格式

  • 生成公式
    JWT_Token=
    	# 标头:Base64仅是字节码编码,不具备加密属性
    	Base64(Header).
    	# 有效载荷:Base64仅是字节码编码,不具备加密属性
    	Base64(Payload).
    	# 签名:前两者组成字符串,并用服务器的私钥secret对其进行加密,加密方式在标头
    	HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
    
  • 组成
    在这里插入图片描述

    jwt官网:传送门,此处可以通过上表生成JWT token

2.2 DRF中实现

2.2.1 jwtToken生成

封装生成jwt Token的相关步骤,插入过期时间间隔,供3.2.3引入

  • /api/auth/createtoken.py
    import jwt
    import datetime
    from django.conf import settings
    
    
    def create_token(payload, timeout=1):
        salt = settings.SECRET_KEY
        # 构造jwt标头
        headers = {
            'typ': 'jwt',
            'alg': 'HS256'
        }
    
        # 构造payload: 增加过期时间,默认1分钟,可传入分钟数
        payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout)
        # 生成jwtToken:jwt模块封装加密方法
        token = jwt.encode(payload=payload, key=salt, algorithm='HS256', headers=headers)
    	# 返回token
        return token
    

2.2.2 jwt Token认证规则

在所有需要认证的API上启用此jwt Token校验,供3.2.3、settings.py引入

  • /api/auth/auth.py
    # 继承Django认证基类,创建自己的认证类
    from rest_framework.authentication import BaseAuthentication
    # 认证未通过则抛出DRF认证失败异常
    from rest_framework.exceptions import AuthenticationFailed
    # 找到settings.py文件的方法,获取里面的Django项目密钥
    from django.conf import settings
    
    import jwt
    # jwt异常抛出
    from jwt import exceptions
    
    
    # 此类在每个需要认证的API最开始执行
    class JwtAuthentication(BaseAuthentication):
        # 创建自己的认证类必须重写此方法
        def authenticate(self, request):
            # 获取并判断token的合法性
            # 1.切割、2.解密第二段/判断是否过期、3.验证第三段合法性
            token = request.headers.get("jwtToken")
            salt = settings.SECRET_KEY
    
            # 解密jwt生成的token,并校验jwt token的签名项
            try:
                payload = jwt.decode(token, salt, algorithms=['HS256'])
            # token过期
            except exceptions.ExpiredSignatureError:
                raise AuthenticationFailed({'code': 702, 'error': "token已失效"})
            # token认证未通过
            except jwt.DecodeError:
                raise AuthenticationFailed({'code': 703, 'error': "token认证失败"})
            # token非法伪造
            except jwt.InvalidTokenError:
                raise AuthenticationFailed({'code': 704, 'error': "非法token"})
    
            # 验证通过,返回元组,括号可省略
            return payload, token
    

2.2.3 登陆、登出、注册视图

此处写此三者的api接口

  • /api/auth/authview.py
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from django.contrib.auth import models, authenticate
    from api.auth.createToken import create_token
    
    
    # 注册账号
    class SignupView(APIView):
        # 局部认证类:无需认证即可访问此视图,优先级高于全局认证类
        authentication_classes = []
    
        def post(self, request, *args, **kwargs):
            # 获取POST请求体内的数据
            user_post = request.data.get('username')
            pwd_post = request.data.get('password')
    
            # 取用Django自带的账号表中的User表
            user_object = models.User.objects.filter(username=user_post).first()
    
            # 若账号在user表中,即搜索到user_post对应username的对象,返回错误
            if user_object:
                # 返回字符串: return Response("用户不存在")
                return Response({'code': 702, 'error': "用户名已存在"})
    
            # 否则创建普通账号:管理员账号未create_super_user
            new_obj = models.User.objects.create_user(username=user_post, password=pwd_post)
            return Response({'code': 200, 'error': "用户创建成功"})
    
    
    # 登陆操作
    class LoginView(APIView):
        # 局部认证类:无需认证即可访问此视图,优先级高于全局认证类
        authentication_classes = []
    
        def post(self, request, *args, **kwargs):
            # 获取POST请求体内的数据
            user_post = request.data.get('username')
            pwd_post = request.data.get('password')
    
            # 取用Django自带的auth组件校验用户名和密码
            user_object = authenticate(username=user_post, password=pwd_post)
    
            # 若账号不在账号表中,即未搜索到user_post对应的username的对象
            if not user_object:
                # 返回字符串: return Response("用户不存在")
                return Response({'code': 701, 'error': "用户名或密码不正确"})
    
            # 通过验证:设置jwtToken,并返回前端
            token = create_token({'id': user_object.id, 'name': user_object.username})
            return Response({'code': 700, 'data': token})
    
    
    # 登出操作:也可以前端直接清空jwtToken即可,此处data填空方便前端制空jwtToken
    class LogoutView(APIView):
        # 局部认证类:未写局部认证类,则采用全局认证类
        def post(self, request, *args, **kwargs):
        	# 不需要认证,任何状态都可以请求,功能是清空浏览器jwtToken
        	authentication_classes = []
            return Response({'code': 200, 'data': 'clear'})
    

2.2.3 全局设置

  • settings.py
    ...
    REST_FRAMEWORK = {
    	# 默认全局认证类:局部认证类优先于全局
        'DEFAULT_AUTHENTICATION_CLASSES': [
        	# 此处找到认证的类
            'api.auth.auth.JwtAuthentication',
        ],
    }
    ...
    
  • urls.py
    ...
    from django.urls import path
    
    urlpatterns = [
        path('login/', api.auth.authview.LoginView.as_view()),
        path('logout/', api.auth.authview.LogoutView.as_view()),
        path('signin/', api.auth.authview.SignupView.as_view()),
    ]
    ...
    

2.3 CORS

2.3.1 概念

  • CORS:跨域资源共享,跨域非简单请求都会先发一个option请求给服务器预检,不通过则后续真实请求被浏览器拦截
  • 简单请求:
    • 方法:GET、HEAD、POST
    • 请求头:Accept、Accept-Language、Content-Language、Content-Type
    • Content-Type字段:text/plain、multipart/form-data、application/x-www-form-urlencoded
  • 复杂请求:非简单请求,服务器检测并响应option请求,浏览器做后续拦截动作
    在这里插入图片描述

2.3.2 解决途径

  • 方法一:开发阶段,前端代理解决,如vue代理传送门2.1节
  • 方法二:生产阶段,后端nginx解决,如nginx反向代理3.3.3节
  • 方法三:生产阶段,纯后端CORS解决方案,每种后端语言都有类似的解决方法,下节

2.3.3 django-cors-headers

  • 安装第三方组件:pip install django-cors-headers
  • main/settings.py
    INSTALLED_APPS = [
    	...
        'corsheaders',
    	...
    ]
    
    MIDDLEWARE = [	# 放在首位
        'corsheaders.middleware.CorsMiddleware',
        ...
    ]
    
    # 白名单域名列表:即前端网址在以下列表中,均可向此后端drf发送请求;
    # 全允许:CORS_ALLOW_ALL_ORIGINS = True,仅开发环境用
    CORS_ALLOWED_ORIGINS = [
        "http://localhost:3000",
        "http://192.168.1.102:3000",
        "http://doubi.com",
    ]
    # 最后面添加白名单请求头字段,非以下请求头字段均被过滤掉
    # 自己家的请求头字段都必须加,否则均丢失,下面是默认的,
    # jwtToken是自己加的
    CORS_ALLOW_HEADERS = [
        "accept",
        "accept-encoding",
        "authorization",
        "content-type",
        "dnt",
        "origin",
        "user-agent",
        "x-csrftoken",
        "x-requested-with",
        "jwtToken"
    ]
    

三、权限分配、频率限制

3.1 权限分配

定义:认证是为了区分登陆与否,并与浏览器约定身份认证规则;权限通过认证用户的身份给其分配每个接口API的增(post)删(delete)改(put/patch)查(get)权限
组管理:将用户分配进相应的组内,给组分配权限,适用于大型API权限管理

3.1.1 user与group

  • 组表与用户表
    # 类Group、User基类都继承了model.Model,所以可以用数据库表方法
    from django.contrib.auth.models import Group, User
    
    # 组表操作:Group.objects即进入组表
    group = Group.objects.all().filter(name="test").first()
    # 用户表操作:User.objects即进入用户表
    user = User.objects.all().filter(username="duke").first()
    
    # 用户调用方法添加、删除组:多次操作无副作用,返回none,
    # 	group也可换成组ID
    user.groups.add(group)
    user.groups.remove(group)
    # 清空用户加入的所有组
    user.groups.clear()
    # 组调用方法添加、删除用户:多次操作无副作用,返回none
    group.user_set.add(user)
    group.user_set.remove(user)
    # 清空组内的所有用户
    group.user_set.clear()
    
  • user表
    在这里插入图片描述
  • group表
    在这里插入图片描述
  • user与group多对多表
    在这里插入图片描述

3.1.2 User替换法扩表

  • 应用场景:Django原表字段过少,需要新增字段
  • 操作:在任意app中的model.py文件中新增如下类
  • login/model.py
    # login是由python manage.py startapp login生成
    from django.contrib.auth.models import AbstractUser
    from django.db import models
    
    
    # 继承AbstractUser,后新增字段即可
    class UserProfiles(AbstractUser):
        # 添加一个字段:新加的都设置可以为空,可以写空格
        telephone = models.CharField(verbose_name='电话', \
        							blank=True, null=True, max_length=15)
        qq = models.CharField(verbose_name='qq', \
        							blank=True, null=True, max_length=15)
    	
        def __str__(self):
            return self.username
    
    # 重写组表
    class GroupProfiles(models.Model):
        name = models.CharField(max_length=12, unique=True)
        UserProfiles = models.ManyToManyField('UserProfiles')
    
  • settings.py
    ...
    # 注册app
    INSTALLED_APPS = [
        ...
        'login',
    	...
    ]
    # 以后用户认证会指向login app的UserProfiles
    AUTH_USER_MODEL = 'login.UserProfiles'
    
  • 迁移数据库
    python manage.py makemigrations
    python manage.py migrate
    

    报错:You are trying to add a non-nullable field ‘password’ to userprofiles without a default;
    解决:按提示选择1后随便设置一个一次性默认值,然后重新运行上面两个命令

  • 密码明文问题
    from login.models import UserProfiles
    from django.contrib.auth.hashers import make_password
    from django.conf import settings
    
    # 替换User后密码会变明文,使用以下Django默认方法加密密码
    passwd = make_password(pwd_post, settings.SECRET_KEY)
    # 此时Django内User表被UserProfiles替换,常规单表方法都可以用了
    UserProfiles.objects.create(username=user_post, password=passwd)
    
    
    ###################################################################
    # 密码验证方法:密码是明文,加密比对由Django实现
    from django.contrib.auth import models, authenticate
    # 验证未通过则user_object为空
    user_object = authenticate(username=user_post, password=pwd_post)
    

3.1.2 权限配置

  • api/permissions/permissions.py
    from rest_framework.permissions import BasePermission
    from django.conf import settings
    import jwt
    from django.contrib.auth.models import User
    
    
    # 封装获取对jwtToken的解码和获取载荷payload的方法类
    class UserAndGroup:
    
        def getusername(self, req):
            token = req.headers.get("jwtToken")
            salt = settings.SECRET_KEY
            # 解码jwtToken,payload内容:print(payload)可以看到全部
            payload = jwt.decode(token, salt, algorithms=['HS256'])
            return payload['name']
    
    
    # 定义权限类,必须继承BasePermission,并重写has_permission方法
    class StuGroupPermission(BasePermission):
    
        def has_permission(self, request, view):
            # 调用上一个类的获取用户名方法
            username = UserAndGroup().getusername(request)
            # 在用户表User中检索对应用户名的记录
            userqueryset = User.objects.all().filter(username=username).first()
            # 判断用户是否在组中:在则返回true,否则False
            if userqueryset.groups.filter(name='stu').exists():
                return True
            return False
    
  • settings.py
    ...
    REST_FRAMEWORK = {
    	 ...
    	 # 全局权限设置
         'DEFAULT_PERMISSION_CLASSES': [
         # AllowAny 允许所有用户
    	 # IsAuthenticated 仅通过认证的用户
    	 # IsAdminUser 仅管理员用户
    	 # IsAuthenticatedOrReadOnly 认证的用户可以完全操作,否则只能get读取
             'rest_framework.permissions.IsAuthenticatedOrReadOnly',
         ],
         ...
    }
    ...
    
  • api/views.py
    from rest_framework import viewsets
    from api import models, serializers
    # 其他参见全局配置
    from rest_framework.permissions import IsAuthenticatedOrReadOnly 
    from api.permissions.permissions import StuGroupPermission
    
    
    class StuViewSet(viewsets.ModelViewSet):
        # 传入instance,关联数据库模型
        queryset = models.Stu.objects.all()
        # 传入serializer,关联序列化器
        serializer_class = serializers.StuSerializer
        # 局部权限设置:优先级高于全局,permission_classes = [],则自由访问
        permission_classes = [StuGroupPermission, ]
    

3.2 频率限制

3.2.1 配置

  • 提示:因考虑移动端、桌面端的公共后端,所以选用jwt认证,因非drf默认认证方式,所以默认的频率控制无法使用,即rest_framework.throttling.UserRateThrottle报错
  • 全局配置settings.py
    REST_FRAMEWORK = {
        ...
        # 选择默认的频率限制类,局部频率限制优先域全局频率限制
        # 省去此项表示全局无限制
        'DEFAULT_THROTTLE_CLASSES': (
        	# common/utils/throttle.py中的类UserThrottle
            'common.utils.throttle.UserThrottle',
        ),
        # 默认频率限制列表:未认证的用户每分钟允许发3次请求
        # 其他单位:s、m、h、d
        'DEFAULT_THROTTLE_RATES': {
            '未认证用户': '3/m',
            '已认证用户': '5/m',
        },
    }
    
  • 限制实现common/utils/throttle.py
    from rest_framework.throttling import SimpleRateThrottle
    
    
    class AnonThrottle(SimpleRateThrottle):
    	# 对应settings.py中的DEFAULT_THROTTLE_RATES的键名
        scope = "未认证用户"
    	# returrn none表示不做限制
    	# return ** 表示以此**为缓存中的监测键名,并以此来区分统计请求
        def get_cache_key(self, request, view):
        	# 此处为获取客户端的真实IP,docker中Nginx需要配置host
            return self.get_ident(request)
    
    
    class UserThrottle(SimpleRateThrottle):
        scope = "已认证用户"
    	# 已认证用户以用户表User中的id为缓存中监测键名,并以此来区分统计请求
        def get_cache_key(self, request, view):
        	# 可以先print(request.user),查看相应字典键名,只要唯一就可以
            return request.user['id']
    

3.2.2 使用

  • 局部使用
    ######################APIView视图写法###########################
    ...
    from common.utils.throttle import AnonThrottle
    
    class LoginView(APIView):
    	# 无需认证和权限限制
        authentication_classes = []
        # 频率:一定要标识权限才生效
        permission_classes = []
        # 局部频率限制:优先级高于全局频率限制
        throttle_classes = [AnonThrottle]
        # 其他正文
        def post(self, request, *args, **kwargs):
        	...
    ...
    #######################viewsets视图写法##########################
    ...
    from common.utils.throttle import AnonThrottle	
    
    class StuViewSet(viewsets.ModelViewSet):
        queryset = models.Stu.objects.all()
        serializer_class = serializers.StuSerializer
        # 频率:一定要标识权限才生效
        permission_classes = [StuGroupPermission, ]
        # 局部频率限制:此处应用AnonThrottle类的限制,
        # 以客户端IP为缓存中的控制键值,每分钟三次请求
        throttle_classes = [AnonThrottle]
        # 为空表示:针对此视图无频率限制
        # throttle_classes = []
    

跳转至总篇目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值