目录
一、认证原理
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的认证流程如下:
- 首次认证段
- 首先,前端post请求将用户名和密码发送到后端接口。常规是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探;
- 后端核对用户名和密码成功后,生成JWT Token;
- 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可;
- 正常通信段
- 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
- 后端查验前端传过来的JWT Token,比如签名是否正确、是否过期、token的接收方是否是自己等等
- 验证通过后,后端解析出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 = []